Multitabs app with real time data: frontend does not refresh properly

Summary

I need to present some values that update every few seconds in a multitabs app using streamlit_option_menu.

Metrics are currently updated in a infinite loop and visualized into a st.empty() placeholder.

Problem: When changing the active tabs, “ghosts” of previous data are “forgotten” in the page because the frontend is never told to remove them.

The problem comes from the usage of st.empty() together with time.sleep that I need to use in order to update the metrics.

Steps to reproduce

import streamlit as st
from streamlit_option_menu import option_menu
import random
import time

st.header("Multipage with data refresh")

page = option_menu("", ["page1", "page2"], orientation="horizontal")
placeholder = st.empty()

if page == "page1":
    while True:
        with placeholder.container():
            st.metric(label="Page 1 Metric 1", value=random.randrange(1000, 1109))
        time.sleep(1)
elif page == "page2":
    while True:
        with placeholder.container():
            st.metric(label="Page 2 Metric 1", value=random.randrange(2000, 2109))
            st.metric(label="Page 2 Metric 2", value=random.randrange(2000, 2109))
            st.metric(label="Page 2 Metric 3", value=random.randrange(2000, 2109))
        time.sleep(1)

Alternative code without the while True loop:

import streamlit as st
from streamlit_option_menu import option_menu
import random
import time

st.header("Multipage with data refresh")

page = option_menu("", ["page1", "page2"], orientation="horizontal")
placeholder = st.empty()

if page == "page1":
    with placeholder.container():
        st.metric(label="Page 1 Metric 1", value=random.randrange(1000, 1109))
elif page == "page2":
    with placeholder.container():
        st.metric(label="Page 2 Metric 1", value=random.randrange(2000, 2109))
        st.metric(label="Page 2 Metric 2", value=random.randrange(2000, 2109))
        st.metric(label="Page 2 Metric 3", value=random.randrange(2000, 2109))
        
time.sleep(2)

To see the problem: Click first on page2 and then page1. You will see the “ghosts” of two metrics that shouldn’t be there.

In the second example the new tab is painted while the frontend has not been told that the old one has to be removed. For a couple of seconds the old metrics are grayed out (backend in time.sleep loop, frontend tells us it is not receiving any update), and then finally removed at the end of the pause.

Question: Is there any way to force the frontend to refresh while in the infinite while True?
As an alternative: how to implement a non-blocking time.sleep(1) to allow backend and frontend to chat and fix all the pending stuff while they wait the pause to end?

Already tried

Multiple placeholders, one for tab

The most obvious solution would be to use TWO placeholders, and empty the unused one each time a tab is activated:

import streamlit as st
from streamlit_option_menu import option_menu
import random
import time

st.header("Multipage with data refresh")

page = option_menu("", ["page1", "page2"], orientation="horizontal")
placeholder1 = st.empty()
placeholder2 = st.empty()

if page == "page1":
    placeholder2.empty()
    while True:
        with placeholder1.container():
            st.metric(label="Page 1 Metric 1", value=random.randrange(1000, 1109))
        time.sleep(1)
elif page == "page2":
    placeholder1.empty()
    while True:
        with placeholder2.container():
            st.metric(label="Page 2 Metric 1", value=random.randrange(2000, 2109))
            st.metric(label="Page 2 Metric 2", value=random.randrange(2000, 2109))
            st.metric(label="Page 2 Metric 3", value=random.randrange(2000, 2109))
        time.sleep(1)

As my app should present around 100-200+ metrics split in a number of tabs, this would mean a HUGE quantity of placeholders to be emptied when changing pages and tabs. As a PLAN B this is the solution I am implementing at the moment, even if it is so cumbersome that some of the elegance of streamlit is lost along the way.

Adding streamlit_autorefresh component

It restarts the app after an interval. These metrics tabs need a frequent update and they share the app with a number of other static pages that don’t need to be updated at all. Continuously restarting the app doesn’t look an optimal solution as it involves loading tons of data from DB that cannot be effectively cached.

  • Streamlit version: 1.15.2
  • Python version: 3.8.10
  • OS version: Ubuntu 20.04
  • Browser version: Firefox 108.0, Brave 1.16.76, Chrome 103.0.5060.114

After 4 days NO answers, I opened an issue
I opened an issue Add st.sleep? #5883 to propose an enhancement: a non-blocking st.sleep managed by streamlit instead of using the standard time.sleep .

1 Like

I’ve had some issues with empty leaving ghosts as well, but if I have the explicity command to re-empty before writing back in (sometimes in just the right spot), I can get around it.

This works for me without ghosting:

import streamlit as st
from streamlit_option_menu import option_menu
import random
import time

st.header("Multipage with data refresh")

page = option_menu("", ["page1", "page2"], orientation="horizontal")
placeholder = st.empty()

if page == "page1":
    placeholder.empty()
    while True:
        with placeholder.container():
            st.metric(label="Page 1 Metric 1", value=random.randrange(1000, 1109))
        time.sleep(1)
elif page == "page2":
    placeholder.empty()
    while True:
        with placeholder.container():
            st.metric(label="Page 2 Metric 1", value=random.randrange(2000, 2109))
            st.metric(label="Page 2 Metric 2", value=random.randrange(2000, 2109))
            st.metric(label="Page 2 Metric 3", value=random.randrange(2000, 2109))
        time.sleep(1)

That the hypothesis I tested. Alas even after a complete clean it doesn’t work and ghosts are there.
I assume it has to do with the blocking while True: .... time.sleep(1) that for some reason stops the communication between frontend and backend.

It is very frustrating, and I had to remove all the real time pages in my app… I now assign a unique placeholder for every page and tab, and their list is saved in st.sessions, to be sure they are not lost. Before assigning every st.empty I clean ALL placeholders with ph.empty() but it doesn’t work and ghosts are there.

I found there is a bug registered on GitHub last night. I ran several different scenarios and found that behavior is inconsistent for many different approaches. The most reliable solution is to put something like time.sleep(.01) immediately after the clearing action, whether that is placeholder.empty() or placeholder.write(' ').

@blackary posted the workaround in the GitHub thread. I posted a gist showing a few different scenarios that I ran with and without the workaround. The most reliable methods I’ve found is to clear the empty element in a callback (either using .empty() method or by writing a blank line, followed by a .01 second sleep) or to write a blank line followed by the .01 second sleep within the same page load you are writing new data. In the case of writing multiple lines within a container within an empty, the empty method is less reliable than writing a blank line.

And for reference, I could produce this bug both with time.sleep and a large counting loop taking approximately the same time, so I’m not sure the bad emptying behavior is derived from time.sleep specifically. However, when I do use longer sleeps, I do it via a loop of shorter sleeps so that Streamlit is able to escape the sleep state from a click in what feels closer to real time. So a Streamlit-controlled sleep function would be a nice feature as you posted on GitHub, too.

2 Likes

@mathcatsand , alas the proposed solution doesn’t work in my case.
Adding time.sleep(0.01) works in small snippets, but not in my full developed app.

empty() is very unreliable.

I take your code, revise it, and put some comments. I have not encountered any issues with it.

import streamlit as st
from streamlit_option_menu import option_menu
import random
import time

st.header("Multipage with data refresh")

page = option_menu("", ["page1", "page2"], orientation="horizontal")
placeholder1 = st.empty()
placeholder2 = st.empty()

if page == "page1":
    # Comment below code. When we change the page, streamlit will
    # rerun the code from top to bottom, which will empty the holders anyway.
    # placeholder2.empty()

    while True:
        with placeholder1.container():
            time.sleep(1)  # simulate that taking data takes time.
            st.metric(label="Page 1 Metric 1", value=random.randrange(1000, 1109))

        # Comment it, do not delay building the container.
        # time.sleep(1)

elif page == "page2":
    # placeholder1.empty()
    while True:
        with placeholder2.container():
            time.sleep(1)
            st.metric(label="Page 2 Metric 1", value=random.randrange(2000, 2109))
            st.metric(label="Page 2 Metric 2", value=random.randrange(2000, 2109))
            st.metric(label="Page 2 Metric 3", value=random.randrange(2000, 2109))
        # time.sleep(1)
1 Like

Sorry, I missed your answer. Is the trick to insert a delay BEFORE updating the components, instead of AFTER?

Based on visual code analysis, the sleep() is delaying the construction of container. We can delay showing the metric but the container should be there ready already.

Have you tried this structure? What is your result?

As I can see it is better, but not safe 100%. Sometimes it doesn’t work, without any regularity.
I was positive about this solution, till I have shown my boss the app, when it didn’t work. Of course in the worse moment.

In the real app I need to suspend the readings for 5 seconds, and in the meantime any action on elements doesn’t work. I can request another page, but till the end of the 5 seconds everything freezes.
I opened an issue to introduce a non-blocking st.sleep to avoid this blocking time.sleep that perhaps is causing a communication delay between backend and frontend, with the placeholder not emptied upon request.
My proposal had no answers yet.

If you need longer times of sleep, try using multiple short sleeps. If I have have a need for time.sleep(10), I instead do a loop of 10 with time.sleep(1). For me, this seems to solve the problem you mentioned of responsiveness while going through longer sleeps.

1 Like

Have you tried doing this with a queue?

What do you mean “with a queue”?

queue

@ferdie , I know what it is a queue :slightly_smiling_face:
I don’t understand how a queue could be used in this case to solve the problem at hand.

Hola, just passing by…

I think it meant managing your 5 second interval readings outside the Streamlit thread. Like in Best (fastest) practice to display live 2D data - #4 by andfanilo you would download the readings every 5 seconds in a separate asyncio thread (which shouldn’t block Streamlit), put them in a queue and have the Streamlit thread pull the new readings from there. That way you can still change page in between those 5 seconds.

It does not solve your ghosting problem though. I personally try to put placeholders as deeply in the tree as possible and only on single elements so the key/label of the widget manages its visibility, not empty full containers/columns (emptying entire containers of widgets, as you can see doesn’t really work out and ghosts of children of the container still persist :frowning:) and reuse widgets as much as possible, like

import streamlit as st
from streamlit_option_menu import option_menu
import random
import time

st.header("Multipage with data refresh")

page = option_menu("", ["page1", "page2"], orientation="horizontal")
placeholder = st.container()
with placeholder:
    m_1 = st.empty()
    m_2 = st.empty()
    m_3 = st.empty()

if page == "page1":
    while True:
        m_1.metric(label="Page 1 Metric 1", value=random.randrange(1000, 1109))
        time.sleep(1)
elif page == "page2":
    while True:
        m_1.metric(label="Page 2 Metric 1", value=random.randrange(2000, 2109))
        m_2.metric(label="Page 2 Metric 2", value=random.randrange(2000, 2109))
        m_3.metric(label="Page 2 Metric 3", value=random.randrange(2000, 2109))
        time.sleep(1)

but I totally understand this can’t easily work for you if you’re trying to build containers with different structures and have to prebuild like 50 metrics/charts :confused: plus I’m not totally convinced quick changes from empty to chart to metric would work.

Haven’t read the full issues on Github to give you a better solution. If I get you a better solution (aside from H2O Wave) I’ll write it down here.

Also, st.experimental_rerun after a time.sleep forces a rerun and sometimes gets rid of ghosts. Sometimes (that’s a trick I used a while back)

Have a nice day,
Fanilo

1 Like