holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.63k stars 505 forks source link

Button remains disabled after callback enables it after interactions with plot #2261

Closed rafgonsi closed 9 months ago

rafgonsi commented 3 years ago

ALL software version info

Python 3.7.6, panel==0.11.3, holoviews==1.14.3, bokeh==2.3.1, datashader==0.12.1

Description of expected behavior and the observed behavior

I want my button to trigger some computations (emulated by sleep here). During those computations the button should be disabled. After computations are finished, the button should be enabled again. I found that in certain situations, I am unable to enable the button.

This bug appears in a very specific case after a complex interaction and not always - as you'll see in a gif below, it takes a couple of repetitions to finally reproduce it. Sometimes I need more clicks, sometimes less. Apologies for MRE taking so much lines, but this is the simplest thing I managed to get.

The necessary things to get this bug:

Complete, minimal, self-contained example code that reproduces the issue

import holoviews as hv
import numpy as np
import panel as pn
import time

from holoviews.operation.datashader import regrid

hv.extension('bokeh')
pn.extension()

freezing_button = pn.widgets.Button(name='Freeze', width=300)
check_button = pn.widgets.Button(name='Check state', width=300)
enable_button = pn.widgets.Button(name='Enable', width=300)
disable_button = pn.widgets.Button(name='Disable', width=300)

ls = np.linspace(0, 10, 200)
xx, yy = np.meshgrid(ls, ls)
plot = hv.Image(np.sin(xx)*np.cos(yy))
regridded_plot = regrid(plot)

def callback(event):
    print("1. Starting callback.")
    button = event.obj
    print(f"2. button.disabled = {button.disabled}")
    button.disabled = True
    time.sleep(0.2)  # this simulates some processing 
    print("3. Now I'm going to enable the button.")
    button.disabled = False
    print("4. Done. Button enabled.")
    print(f"5. button.disabled = {button.disabled}\n")

freezing_button.on_click(callback)

def check_callback(event):
    print(f"-. freezing_button.disabled = {freezing_button.disabled}")

check_button.on_click(check_callback)

def enable_callback(event):
    freezing_button.disabled = False

enable_button.on_click(enable_callback)

def disable_callback(event):
    freezing_button.disabled = True

disable_button.on_click(disable_callback)

pn.Column(check_button, enable_button, disable_button, freezing_button, regridded_plot).servable()

Screenshots or screencasts of the bug in action

Peek 2021-04-27 12-11

xavArtley commented 3 years ago

If you use instructions here https://panel.holoviz.org/user_guide/Async_and_Concurrency.html can you reproduce?

import holoviews as hv
import numpy as np
import panel as pn
import time
import asyncio

from holoviews.operation.datashader import regrid

pn.extension()

freezing_button = pn.widgets.Button(name='Freeze', width=300)
check_button = pn.widgets.Button(name='Check state', width=300)
enable_button = pn.widgets.Button(name='Enable', width=300)
disable_button = pn.widgets.Button(name='Disable', width=300)

ls = np.linspace(0, 10, 200)
xx, yy = np.meshgrid(ls, ls)
plot = hv.Image(np.sin(xx)*np.cos(yy))
regridded_plot = regrid(plot)

async def callback(event):
    print("1. Starting callback.")
    button = event.obj
    print(f"2. button.disabled = {button.disabled}")
    button.disabled = True
    await asyncio.sleep(0.2)
    print("3. Now I'm going to enable the button.")
    button.disabled = False
    print("4. Done. Button enabled.")
    print(f"5. button.disabled = {button.disabled}\n")

freezing_button.on_click(callback)

def check_callback(event):
    print(f"-. freezing_button.disabled = {freezing_button.disabled}")

check_button.on_click(check_callback)

def enable_callback(event):
    freezing_button.disabled = False

enable_button.on_click(enable_callback)

def disable_callback(event):
    freezing_button.disabled = True

disable_button.on_click(disable_callback)

pn.Column(check_button, enable_button, disable_button, freezing_button, regridded_plot).servable()
rafgonsi commented 3 years ago

The error does not appear anymore when using this approach, but unfortunately it works correctly only in the simple case of await asyncio.sleep(0.2). When I replace it by some cpu heavy operation, then the button does not become grey or becomes grey only for a blink of an eye after the computations are done. See gif below. The only thing I changed in your code is replacing asyncio.sleeping by tmp = np.random.rand(1000, 1000, 300).

Peek 2021-04-29 09-58

Also after some time I see the following message in the console:

bokeh.document.document - ERROR - Module <module 'bokeh_app_d4b8673bb7cf43a4b332373c309d962d' from '/bug.py'> has extra unexpected referrers! This could indicate a serious memory leak. Extra referrers: [<cell at 0x7f6c34e07850: module object at 0x7f6c34e0e170>]

philippjfr commented 9 months ago

I tried my best but couldn't reproduce this issue anymore, which hopefully means it was fixed some time in the intervening 2.5 years.