Solved

Streamlit - Async tasks problem

  • 4 September 2023
  • 4 replies
  • 880 views

Userlevel 1
Badge +5

Currently Cognite’s Streamlit apps have issues with for example:

with st.spinner('Wait for it...'):
time.sleep(5)
st.success('Done!')

The code itself should wait 5 seconds before writing out Done!, but is executed instantly. 

Another example is when you try to make your application responsive while waiting for some data to arrive from a remote location. For example, when you want to print out a wait message for the user because sometimes it takes some seconds to load the data, but the app doesn't work as intended. In the following example I am trying to print out a message while waiting for the function to finish, but it fails:

call_func_button = st.button("Call function with current parameters", key="button_func_call")


if call_func_button:
func_call = example_function.call(data=input_data, wait=False)
st.info("Waiting for function...")
while func_call.status == "Running":

func_call.update()

if func_call.status == "Failed":
st.error("Function failed to run!")

elif func_call.status == "Completed":
runtime = float(func_call.end_time - func_call.start_time) / 1000.0
st.success(f"Function ran successfully in {runtime} seconds!")

The line "Waiting for function..." does not appear during execution, but instead appears when the function completes.

I believe the problem lies with the threading of Streamlit and potentially some config update could change this behavior. 
 

​Thank you,

Tibor Váncza

icon

Best answer by Christian Flasshoff 5 September 2023, 13:39

View original

4 replies

Hi Tibor,

Streamlit's primary mode of operation is based on scripting, meaning it runs from top to bottom each time there's an interaction, updating all the components. In the examples you provided, Streamlit won't wait for things like `time.sleep()` or loop delays in the way that you're expecting.

For your specific examples:

1. The `st.spinner('Wait for it...')` with the `time.sleep(5)` will not display the spinner for 5 seconds before showing "Done!". Instead, Streamlit will finish the entire script execution (including the sleep time) and then update the app interface.

2. In your second example, the loop keeps the script busy until `func_call.status` is no longer "Running". Only after the loop finishes does Streamlit get a chance to update the interface, causing the "Waiting for function..." to appear briefly or not at all, depending on how quickly the subsequent updates occur.

To achieve the desired effect in Streamlit:

1. For the spinner example: There's no direct way to make `st.spinner` wait for a specific time or function. Instead, you'd need to think in terms of breaking the process down into more granular updates, or consider different UI/UX patterns, such as informing users about typical wait times. This is another resource that might help: https://discuss.streamlit.io/t/how-can-i-use-time-sleep-only-for-success-warning-and-info/32788/2

2. For the function call example: One approach is to use Streamlit's caching mechanism. You can cache the function call and let the function be called asynchronously. When the function is being executed (and thus the cache is being populated), you can provide a status message to the user. I have not tested caching with Streamlit running in the browser.

Here's a potential way to implement the second example using Streamlit's `@st.cache`:

import streamlit as st

# Allow_output_mutation lets the object being cached mutate between calls.
# Supposing that example_function.call returns an object that gets updated.
@st.cache(allow_output_mutation=True, show_spinner=True)
def cached_function_call(data):
return example_function.call(data=input_data)

call_func_button = st.button("Call function with current parameters", key="button_func_call")

if call_func_button:
func_call = cached_function_call(input_data)

if func_call.status == "Failed":
st.error("Function failed to run!")
elif func_call.status == "Completed":
runtime = float(func_call.end_time - func_call.start_time) / 1000.0
st.success(f"Function ran successfully in {runtime} seconds!")

Let me know, if this answers your question and solves the problem.

 

Best,

Christian 

Userlevel 1
Badge +5

Some type of difference has to be present. Both the Streamlit documentation and other examples shows us that the intended way of using for example st.spinner is the way I have shown you. Of course, I know that Streamlit reruns parts of the code from top to end when it re-renders the UI, and I assume the re-render is triggered by the user interaction on the UI widgets. But for example, 


import streamlit as st
import pandas as pd
import time

df = pd.read_csv("iris.csv")

col = st.sidebar.multiselect("Select any column", df.columns)

with st.spinner("Just a moment ..."):
time.sleep(1)
st.success('Done!')
st.dataframe(df[col])

This code for example is not working as you described to me in other sources of Streamlit, but rather the st.spinner appears until the code underneath it runs, then disappears.

 

I can give you another example:
 

 

Thanks for adding this example. Streamlit in the browser (running with pyodide) does not work the same as a local run of Streamlit. The GH issue below addresses your problem and also suggests a solution by using an async function (with asyncio library). I’ve tested this example in Streamlit within CDF and it works.
Related GH issue: https://github.com/whitphx/stlite/issues/34#issuecomment-1475722165

Let me know if this solves your case.

 

 

Userlevel 1
Badge +5

Hi!

The solution you’ve provided works! I’ve also suspected this but didn’t find the solution myself. 

Thank you for your time, I am sure this will be helpful to others as well. 🙂

Reply