holoviz / panel

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

Provide example of asynchronous and concurrent tasks using unsync #2792

Open MarcSkovMadsen opened 3 years ago

MarcSkovMadsen commented 3 years ago

As your experience with Panel grows you start to want to make highly performant Panel apps. There is not so much information on this out there.

Panel provides the guide Async and Concurrency.

An approach that can combine async, threading and multiprocessing seems to be unsync.

It is discussed here https://discourse.holoviz.org/t/async-and-concurrency-for-dummies/2546.

@danmaty and I are discussing if we could provide something useful for Panel like a Gallery Example.

Marc Wishes

Example

From https://discourse.holoviz.org/t/async-and-concurrency-for-dummies/2546/9?u=marc

https://user-images.githubusercontent.com/42288570/135205265-7dea5251-1e8f-4fb6-bd30-cc2fb083650f.mp4

import panel as pn
import param
from unsync import unsync
from time import sleep as sl
from time import strftime as st
from panel.layout.gridstack import GridStack

CSS = """
#header {
    background-color: rgba(255,255,255,0.33);
    -ms-box-shadow: none !important;
    -o-box-shadow: none !important;
    -moz-box-shadow: none !important;
    -webkit-box-shadow: none !important;
    box-shadow: none !important;
}

.main-content {
    transition: all 0.2s cubic-bezier(0.945, 0.020, 0.270, 0.665);
    width: 100%;
    height: calc(100vh - 76px);
    padding: 10px;
}

body {
    background: radial-gradient(#B8BEB4, #71685F);
}

.bk-root button.button {
    width:65px;
    height:55px;
    position:absolute;
    bottom:0px;
    background-color: rgba(255,255,255,0.33);
    color:#FFF;
    border-radius:25px;
    text-align:center;
    box-shadow: 2px 2px 3px black;
    transition: all 0.2s ease-in-out;
    font-size:30px;
    border-color: rgba(255,255,255,0.33);
}

.bk-root button.button:hover {
    box-shadow: 4px 4px 3px black;
    background-color: rgba(255,255,255,0.33);
    transform: scale(1.05);
    border-color: rgba(255,255,255,0.33);
    cursor: pointer;
}

.bk-root button.button:active {
    transform: translateY(1px);
}
"""

pn.config.sizing_mode = 'stretch_both'
pn.extension('gridstack', raw_css=[CSS])

playSVG="""<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="48px" viewBox="0 0 20 20" width="48px" fill="#000000"><g><rect fill="none" height="20" width="20"/></g><g><g><path d="M10,2c-4.42,0-8,3.58-8,8s3.58,8,8,8s8-3.58,8-8S14.42,2,10,2z M10,16.5c-3.58,0-6.5-2.92-6.5-6.5S6.42,3.5,10,3.5 s6.5,2.92,6.5,6.5S13.58,16.5,10,16.5z"/><polygon points="8,13.5 13.5,10 8,6.5"/></g></g></svg>
"""
pauseSVG="""<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
"""
stopSVG="""<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 8v8H8V8h8m2-2H6v12h12V6z"/></svg>
"""

@unsync
def unsync_func1(name):
    global stop_unsync1
    print('unsync1', name)
    if name == 'play':
        stop_unsync1 = False
        while 1:
            if stop_unsync1 == True:
                break
            time_unsync1 = st("%H:%M:%S")
            unsync_out1.object = f"<h1>{time_unsync1}</h1>"
            sl(0.1)
            # print('unsync1 running...')
    elif name == 'pause':
        stop_unsync1 = True
    elif name == 'stop':
        stop_unsync1 = True
        unsync_out1.object = "<h1>00:00:00</h1>"

@unsync
def unsync_func2(name):
    global stop_unsync2
    print('unsync2', name)
    if name == 'play':
        stop_unsync2 = False
        while 1:
            if stop_unsync2 == True:
                break
            time_unsync2 = st("%H:%M:%S")
            unsync_out2.object = f"<h1>{time_unsync2}</h1>"
            sl(0.1)
            # print('unsync2 running...')
    elif name == 'pause':
        stop_unsync2 = True
    elif name == 'stop':
        stop_unsync2 = True
        unsync_out2.object = "<h1>00:00:00</h1>"

@unsync
def unsync_func3(name):
    global stop_unsync3
    print('unsync3', name)
    if name == 'play':
        stop_unsync3 = False
        while 1:
            if stop_unsync3 == True:
                break
            time_unsync3 = st("%H:%M:%S")
            unsync_out3.object = f"<h1>{time_unsync3}</h1>"
            sl(0.1)
            # print('unsync3 running...')
    elif name == 'pause':
        stop_unsync3 = True
    elif name == 'stop':
        stop_unsync3 = True
        unsync_out3.object = "<h1>00:00:00</h1>"

class SVGButton(pn.reactive.ReactiveHTML):
    svg = param.String(doc="The SVG")
    name = param.String(doc='Icon ID')
    func_type = param.String(doc='Type of function')

    _template = """<button class="button" type="button" id="button" onclick="${_do}">{{svg}}</button>"""

    def _do(self, _):
        if self.func_type == 'unsync1':
            unsync_func1(self.name)
        elif self.func_type == 'unsync2':
            unsync_func2(self.name)
        elif self.func_type == 'unsync3':
            unsync_func3(self.name)

play_button_unsync1 = SVGButton(svg=playSVG, name='play', func_type='unsync1', height=60, width=60)
pause_button_unsync1 = SVGButton(svg=pauseSVG, name='pause', func_type='unsync1', height=60, width=60)
stop_button_unsync1 = SVGButton(svg=stopSVG, name='stop', func_type='unsync1', height=60, width=60)
unsync_buttons1 = pn.Row(play_button_unsync1, pause_button_unsync1, stop_button_unsync1)

play_button_unsync2 = SVGButton(svg=playSVG, name='play', func_type='unsync2', height=60, width=60)
pause_button_unsync2 = SVGButton(svg=pauseSVG, name='pause', func_type='unsync2', height=60, width=60)
stop_button_unsync2 = SVGButton(svg=stopSVG, name='stop', func_type='unsync2', height=60, width=60)
unsync_buttons2 = pn.Row(play_button_unsync2, pause_button_unsync2, stop_button_unsync2)

play_button_unsync3 = SVGButton(svg=playSVG, name='play', func_type='unsync3', height=60, width=60)
pause_button_unsync3 = SVGButton(svg=pauseSVG, name='pause', func_type='unsync3', height=60, width=60)
stop_button_unsync3 = SVGButton(svg=stopSVG, name='stop', func_type='unsync3', height=60, width=60)
unsync_buttons3 = pn.Row(play_button_unsync3, pause_button_unsync3, stop_button_unsync3)

unsync_out1 = pn.pane.HTML(object="""<h1>00:00:00</h1>""")
unsync_out2 = pn.pane.HTML(object="""<h1>00:00:00</h1>""")
unsync_out3 = pn.pane.HTML(object="""<h1>00:00:00</h1>""")

card_bg='rgba(255,255,255,0.33)'

gs = GridStack(sizing_mode='stretch_both', ncols=5, nrows=3, height=500, allow_resize=False, allow_drag=False)

gs[0:1, 0:5] = pn.Spacer(margin=5)
gs[1:2, 1:2] = pn.Card(unsync_out1, title='Unsync Function 1', background=card_bg, collapsible=False)
gs[1:2, 2:3] = pn.Card(unsync_out2, title='Unsync Function 2', background=card_bg, collapsible=False)
gs[1:2, 3:4] = pn.Card(unsync_out3, title='Unsync Function 3', background=card_bg, collapsible=False)
gs[2:3, 1:2] = unsync_buttons1
gs[2:3, 2:3] = unsync_buttons2
gs[2:3, 3:4] = unsync_buttons3

mt = pn.template.MaterialTemplate(
    header_background='rgba(255,255,255,0.33)',
    title='Unsync Demo',
    main=[gs],
).servable()

App Ideas

MarcSkovMadsen commented 3 years ago

Hi @danmaty

My comments to your example are

Could we make a "real" example solving some problem that uses unsync?

How do we know that recommending this approach is a good idea? is unsync a robust, well maintained library? And can we expect it to stay that?

danmaty commented 3 years ago

Hi @MarcSkovMadsen

I can't confirm whether or not Unsync is well maintained. That might be a question for @alex-sherman to answer.

I've made another very simple example with longer background processes utilizing Pandas + IMDB data and Pandas-profiling + Numpy.

To me Panel apps are always about user experience. How does the app feel and look. In my opinion a very obvious goal for the tool should be to make it possible to style it easily and to achieve a more dynamic feel. It's already possible to do these to some extent but not necessarily in a straightforward way.

So this example has three tabs. App initial load time is quick because it renders without value/object on the second and third tab and the first one is essentially just distraction. This is to provide more ideas on how to utilize Unsync to prevent Panel's potentially long initial load and to keep the app responsive / alive while contents are being generated in the background obviously in the most ideal/prioritized order. So the user can experience on-demand data as much as it is possible.

See attached code. Not sure how to highlight...

import panel as pn
import param
import pandas as pd
import numpy as np
from pandas_profiling import ProfileReport
from unsync import unsync
from time import time as tt

pn.config.sizing_mode='stretch_both'
pn.extension('tabulator')

@unsync
def make_html():
    start = tt()
    df = pd.DataFrame(np.random.rand(100, 5), columns=["a", "b", "c", "d", "e"])

    profile = ProfileReport(df, title="Pandas Profiling Report", explorative=False)
    html = profile.to_html()

    html_pane.object = html
    print('make_html', tt() - start)

@unsync
def make_table():
    start = tt()
    link = 'https://datasets.imdbws.com/title.ratings.tsv.gz'
    link2 = 'https://datasets.imdbws.com/title.episode.tsv.gz'

    df = pd.read_csv(link, compression='gzip', sep='\t').describe()

    tabulator_pane.value = df
    print('make_table', tt() - start)

html_pane = pn.pane.HTML()
tabulator_pane = pn.widgets.Tabulator()
str_pane = pn.pane.Str('Blah blah blah')

make_html()
make_table()

tabulator_pane = pn.widgets.Tabulator()

pn.layout.Tabs(('Intro', str_pane), ('Table', tabulator_pane), ('HTML', html_pane)).show()
philippjfr commented 3 years ago

Unsync certainly looks interesting but there are many different such solutions and I do not think such an example belongs in Panel itself. We'd be happy to host such an example on https://github.com/pyviz-topics/examples or somewhere else and link it from the Panel user guide and gallery however. Do not want to make this decision unilaterally however. Any thoughts @maximlt and @jbednar ?

jbednar commented 3 years ago

I'd be very happy to have a solid, performant, and ambitious async Panel app to host on examples.pyviz.org!

alex-sherman commented 3 years ago

is unsync a robust, well maintained library? And can we expect it to stay that?

Yeah I think it's well maintained, but obviously biased. It hasn't changed for quite some time, and I only expect to get occasional bugs from new Python versions etc. Realistically it's <200 lines so not much to maintain anyway 😉