holoviz / holoviews

With Holoviews, your data visualizes itself.
https://holoviews.org
BSD 3-Clause "New" or "Revised" License
2.69k stars 402 forks source link

Make bokeh serve app faster / profile performance #3394

Open flothesof opened 5 years ago

flothesof commented 5 years ago

Hi there,

I've written a short app that solves the 2D wave equation and where you can create waves by tapping the image, much in the spirit of the Game of Life App (which I used as a starting point).

However, when I run it using bokeh serve app.py, it quickly starts "lagging", in the sense that the updates don't seem to be happening regularly. Is there a way of making the app run "faster" or profiling the app to understand what is causing the slowdowns? I know that this is not the core objectif of Holoviews, but I thought the question would still be worthwhile to be asked.

The source for the wave equation app is below.

Thank you for your help. Regards, Florian

import numpy as np
import holoviews as hv
from holoviews.streams import Tap, Counter
from holoviews import opts
from numba import jit

renderer = hv.renderer('bokeh')

@jit
def step(wave_fields):
    """2D scalar wave equation step."""
    curr, prev = wave_fields
    next_wave = 2 * curr - prev
    next_wave[1:-1, 1:-1] += .25 * (curr[2:, 1:-1] + curr[:-2, 1:-1] + curr[1:-1, 2:] + curr[1:-1, :-2] - 4 * curr[1:-1, 1:-1])
    next_wave[0, :] = next_wave[-1, :] = next_wave[:, 0] = next_wave[:, -1] = 0
    return next_wave

@jit
def step_attenuation(wave_fields, alpha = 0.97):
    """2D scalar wave equation step with attenuation.

    Attenuation is global and should be smaller than 1.
    """
    curr, prev = wave_fields
    next_wave = 2 * curr - prev
    next_wave[1:-1, 1:-1] += .25 * (
                curr[2:, 1:-1] + curr[:-2, 1:-1] + curr[1:-1, 2:] + curr[1:-1, :-2] - 4 * curr[1:-1, 1:-1])
    next_wave *= alpha
    next_wave[0, :] = next_wave[-1, :] = next_wave[:, 0] = next_wave[:, -1] = 0
    return next_wave

def update(propagator, source, counter, x, y):
    if x and y:
        # create a wave source
        y, x = img.sheet2matrixidx(x,y)
        sources[source](wave_fields[0], x, y)
    else:
        # propagate the wave
        next_wave = propagators[propagator]()
        wave_fields[1] = wave_fields[0]
        wave_fields[0] = next_wave
        img.data = next_wave
    return hv.Image(img)

@jit
def set_simple_source(wave_field, x, y):
    wave_field[y - 1:y + 2, x - 1:x + 2] = [[0, 1, 0], [1, 0, 1], [0, 1, 0]]

@jit
def set_low_freq_source(wave_field, x, y):
    R = np.sqrt((X - x)**2 + (Y - y)**2)
    wave_field += np.exp(-R**2*0.5)

title = '2D wave propagation - tap to create wave source.'
img_opts = opts.Image(height=800, width=800, toolbar=None, xaxis=None, yaxis=None,
                      cmap='seismic', title=title)
wave_fields = [np.zeros((200, 200), dtype=np.float),
               np.zeros((200, 200), dtype=np.float)]
img = hv.Image(wave_fields[0])
X, Y = np.meshgrid(np.arange(img.data.shape[0]), np.arange(img.data.shape[1]))
counter, tap = Counter(transient=True), Tap(transient=True)
propagators = {'no attenuation': lambda: step(wave_fields),
               'small attenuation': lambda : step_attenuation(wave_fields, 0.99),
               'large attenuation': lambda : step_attenuation(wave_fields, 0.95)}
propagation_dim = hv.Dimension('Propagation type', values=list(propagators.keys()))
sources = {'simple source': set_simple_source,
           'low frequency source': set_low_freq_source}
source_dim = hv.Dimension('Source type', values=list(sources.keys()))
dmap = hv.DynamicMap(update, kdims=[propagation_dim, source_dim], streams=[counter, tap])

doc = renderer.server_doc(dmap.redim.range(z=(-0.1, .1)).opts(img_opts))
dmap.periodic(0.05, None)
doc.title = '2D wave propagation'
philippjfr commented 5 years ago

Really cool example! The 50 millisecond update interval is probably a bit too fast and it's accumulating events. We can try to optimize it a bit but one good solution might be to add an 'adaptive' option to the periodic utility, which would make it record the time between updates and then adaptively adjust the timeout to ensure that events don't queue up.

philippjfr commented 5 years ago

So based on my local profiling there really isn't much to optimize here, indeed my machine seems to be able to keep up with the 50 millisecond timeout:

         34568 function calls (34043 primitive calls) in 0.030 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.030    0.030 {built-in method builtins.exec}
        1    0.000    0.000    0.030    0.030 <string>:2(<module>)
        1    0.000    0.000    0.030    0.030 streams.py:367(event)
        1    0.000    0.000    0.030    0.030 streams.py:127(trigger)
        1    0.000    0.000    0.030    0.030 plot.py:601(refresh)
        1    0.000    0.000    0.027    0.027 plot.py:620(_trigger_refresh)
        1    0.000    0.000    0.027    0.027 plot.py:593(update)
        1    0.000    0.000    0.027    0.027 plot.py:252(__getitem__)
        1    0.000    0.000    0.027    0.027 element.py:1101(update_frame)
        3    0.000    0.000    0.011    0.004 plot.py:867(_get_frame)
        1    0.000    0.000    0.011    0.011 util.py:253(get_plot_frame)
      3/1    0.000    0.000    0.011    0.011 spaces.py:1258(__getitem__)
      3/1    0.000    0.000    0.010    0.010 spaces.py:1067(_execute_callback)
      3/1    0.000    0.000    0.010    0.010 spaces.py:675(__call__)
      2/1    0.000    0.000    0.009    0.009 __init__.py:815(dynamic_operation)
       46    0.000    0.000    0.005    0.000 parameterized.py:1988(__init__)
   138/92    0.000    0.000    0.005    0.000 parameterized.py:774(override_initialization)
        1    0.000    0.000    0.005    0.005 element.py:1070(_update_glyphs)
       46    0.001    0.000    0.004    0.000 parameterized.py:885(_setup_params)
        1    0.000    0.000    0.004    0.004 element.py:576(_update_plot)
        2    0.000    0.000    0.004    0.002 __init__.py:795(_process)
        2    0.000    0.000    0.004    0.002 raster.py:264(__init__)
       25    0.000    0.000    0.004    0.000 options.py:534(__init__)
        1    0.000    0.000    0.004    0.004 raster.py:42(get_data)
        1    0.000    0.000    0.004    0.004 element.py:1389(_get_colormapper)
        6    0.000    0.000    0.003    0.001 plot.py:97(lookup_options)
        1    0.000    0.000    0.003    0.003 plot.py:352(compute_ranges)
        1    0.000    0.000    0.003    0.003 util.py:855(process_cmap)
        1    0.000    0.000    0.003    0.003 plot.py:442(_compute_group_range)
        2    0.000    0.000    0.003    0.002 util.py:444(recursive_model_update)
        2    0.000    0.000    0.003    0.002 has_props.py:497(properties_with_values)
        2    0.000    0.000    0.003    0.002 has_props.py:529(query_properties_with_values)
        6    0.000    0.000    0.003    0.000 options.py:1315(lookup_options)
        6    0.000    0.000    0.003    0.000 options.py:820(closest)
        1    0.000    0.000    0.003    0.003 <ipython-input-1-b909214f8939>:35(update)
        1    0.000    0.000    0.003    0.003 plot.py:200(push)
philippjfr commented 5 years ago

I also don't think numba is doing much in your code since the numpy code is already vectorized.

flothesof commented 5 years ago

Thanks for your reply. I get results similar to yours when I profile on my machine: the update step should, according to the profiler, be able to keep up with the 50 ms schedule. But in practice, it doesn't. Do you also observe some sort of lags? I wonder if there would be an option in the bokeh server to somehow be faster? Or has the server nothing to do per se in the final performance?

philippjfr commented 5 years ago

Do you also observe some sort of lags?

When starting up it briefly lags a bit but then seems to keep up pretty well. Bokeh server is fairly well optimized, so I can only imagine that the browser side rendering is a bit slow maybe. Might be worth using the browser profiler tools to see if the rendering is keeping up.