Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/performance_checker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Performance checker Example

This example measures the performance of a stremlit panel with a graph.

## Features

- Generates sine wave data with varying frequency
- Displays the data in a graph
- Updates rapidly
- Shows timing information

### Required Software

- Python 3.9 or later

### Usage

Run `poetry run python examples/performance_checker/performance_checker.py`
69 changes: 69 additions & 0 deletions examples/performance_checker/performance_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""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
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")

# 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...")
126 changes: 126 additions & 0 deletions examples/performance_checker/performance_checker_panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""A Streamlit visualization panel for the performance_checker.py example script."""

import statistics
import time
from typing import Any, Tuple

import streamlit as st
from streamlit_echarts import st_echarts

import nipanel


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.

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 = [] # List of tuples (timestamp, refresh_time_ms)

# 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 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 = [item[1] for item in 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
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)]

# 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 = st.columns(3)
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_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")
st.metric("get unset_value", f"{unset_value_ms:.1f} ms")
3 changes: 1 addition & 2 deletions protos/ni/pythonpanel/v1/python_panel_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -99,7 +98,7 @@ message GetValueRequest {

message GetValueResponse {
// The value
google.protobuf.Any value = 1;
optional google.protobuf.Any value = 1;
}

message SetValueRequest {
Expand Down
16 changes: 8 additions & 8 deletions src/ni/pythonpanel/v1/python_panel_service_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/ni/pythonpanel/v1/python_panel_service_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!')
Expand Down
3 changes: 0 additions & 3 deletions src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand Down Expand Up @@ -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[
Expand Down Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions src/nipanel/_panel_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,20 +116,23 @@ 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:
panel_id: The ID of the panel.
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:
Expand Down
18 changes: 8 additions & 10 deletions src/nipanel/_panel_value_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/test_panel_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import grpc
import pytest

from nipanel._panel_client import PanelClient

Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_streamlit_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
Loading