Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speed up dynamic layout updates #4989

Merged
merged 7 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion panel/io/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,9 @@ def script_to_html(
# Collect resources
resources = Resources(mode='inline' if inline else 'cdn')
loading_base = (DIST_DIR / "css" / "loading.css").read_text(encoding='utf-8')
spinner_css = loading_css()
spinner_css = loading_css(
config.loading_spinner, config.loading_color, config.loading_max_height
)
css_resources.append(
f'<style type="text/css">\n{loading_base}\n{spinner_css}\n</style>'
)
Expand Down
17 changes: 10 additions & 7 deletions panel/io/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from base64 import b64encode
from collections import OrderedDict
from contextlib import contextmanager
from functools import lru_cache
from pathlib import Path
from typing import (
TYPE_CHECKING, Dict, List, Literal, TypedDict,
Expand Down Expand Up @@ -169,15 +170,15 @@ def process_raw_css(raw_css):
"""
return [BK_PREFIX_RE.sub('.', css) for css in raw_css]

def loading_css():
from ..config import config
with open(ASSETS_DIR / f'{config.loading_spinner}_spinner.svg', encoding='utf-8') as f:
svg = f.read().replace('\n', '').format(color=config.loading_color)
@lru_cache(maxsize=None)
def loading_css(loading_spinner, color, max_height):
with open(ASSETS_DIR / f'{loading_spinner}_spinner.svg', encoding='utf-8') as f:
svg = f.read().replace('\n', '').format(color=color)
b64 = b64encode(svg.encode('utf-8')).decode('utf-8')
return textwrap.dedent(f"""
:host(.{LOADING_INDICATOR_CSS_CLASS}.pn-{config.loading_spinner}):before, .pn-loading.pn-{config.loading_spinner}:before {{
:host(.{LOADING_INDICATOR_CSS_CLASS}.pn-{loading_spinner}):before, .pn-loading.pn-{loading_spinner}:before {{
background-image: url("data:image/svg+xml;base64,{b64}");
background-size: auto calc(min(50%, {config.loading_max_height}px));
background-size: auto calc(min(50%, {max_height}px));
}}""")

def resolve_custom_path(
Expand Down Expand Up @@ -673,7 +674,9 @@ def css_raw(self):
# Add loading spinner
if config.global_loading_spinner:
loading_base = (DIST_DIR / "css" / "loading.css").read_text(encoding='utf-8')
raw.extend([loading_base, loading_css()])
raw.extend([loading_base, loading_css(
config.loading_spinner, config.loading_color, config.loading_max_height
)])
return raw + process_raw_css(config.raw_css) + process_raw_css(config.global_css)

@property
Expand Down
5 changes: 3 additions & 2 deletions panel/layout/accordion.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
models and cleaning up any dropped objects.
"""
from panel.pane.base import RerenderError, panel
new_models = []
new_models, old_models = [], []
if len(self._names) != len(self):
raise ValueError(
'Accordion names do not match objects, ensure that the '
Expand Down Expand Up @@ -130,6 +130,7 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
self._panels[id(pane)] = card
if ref in card._models:
panel = card._models[ref][0]
old_models.append(panel)
else:
try:
panel = card._get_model(doc, root, model, comm)
Expand All @@ -146,7 +147,7 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
self._set_active()
self._update_cards()
self._update_active()
return new_models
return new_models, old_models

def _compute_sizing_mode(self, children, props):
children = [subchild for child in children for subchild in child.children[1:]]
Expand Down
35 changes: 21 additions & 14 deletions panel/layout/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ def _update_model(
obj_key = self._property_mapping['objects']
if obj_key in msg:
old = events['objects'].old
msg[obj_key] = children = self._get_objects(model, old, doc, root, comm)
children, old_children = self._get_objects(model, old, doc, root, comm)
msg[obj_key] = children

msg.update(self._compute_sizing_mode(
children,
dict(
Expand All @@ -102,18 +104,21 @@ def _update_model(
margin=msg.get('margin', model.margin)
)
))
else:
old_children = None

with hold(doc):
update = Panel._batch_update
Panel._batch_update = True
try:
super()._update_model(events, msg, root, model, doc, comm)
if update:
return
from ..io import state
ref = root.ref['id']
if ref in state._views and preprocess:
state._views[ref][0]._preprocess(root)
with doc.models.freeze():
super()._update_model(events, msg, root, model, doc, comm)
if update:
return
from ..io import state
ref = root.ref['id']
if ref in state._views and preprocess:
state._views[ref][0]._preprocess(root, self, old_children)
finally:
Panel._batch_update = update

Expand All @@ -130,7 +135,7 @@ def _get_objects(
models and cleaning up any dropped objects.
"""
from ..pane.base import RerenderError, panel
new_models = []
new_models, old_models = [], []
for i, pane in enumerate(self.objects):
pane = panel(pane)
self.objects[i] = pane
Expand All @@ -144,13 +149,14 @@ def _get_objects(
for i, pane in enumerate(self.objects):
if pane in old_objects and ref in pane._models:
child, _ = pane._models[root.ref['id']]
old_models.append(child)
else:
try:
child = pane._get_model(doc, root, model, comm)
except RerenderError:
return self._get_objects(model, current_objects[:i], doc, root, comm)
new_models.append(child)
return new_models
return new_models, old_models

def _get_model(
self, doc: Document, root: Optional[Model] = None,
Expand All @@ -161,7 +167,7 @@ def _get_model(
model = self._bokeh_model()
root = root or model
self._models[root.ref['id']] = (model, parent)
objects = self._get_objects(model, [], doc, root, comm)
objects, _ = self._get_objects(model, [], doc, root, comm)
props = self._get_properties(doc)
props[self._property_mapping['objects']] = objects
props.update(self._compute_sizing_mode(objects, props))
Expand Down Expand Up @@ -206,9 +212,10 @@ def _compute_sizing_mode(self, children, props):
heights, widths = [], []
all_expand_width, all_expand_height, expand_width, expand_height, scale = True, True, False, False, False
for child in children:
if child.sizing_mode and 'scale' in child.sizing_mode:
smode = child.sizing_mode
if smode and 'scale' in smode:
scale = True
if child.sizing_mode in ('stretch_width', 'stretch_both', 'scale_width', 'scale_both'):
if smode in ('stretch_width', 'stretch_both', 'scale_width', 'scale_both'):
expand_width = True
else:
width = child.width or child.min_width
Expand All @@ -222,7 +229,7 @@ def _compute_sizing_mode(self, children, props):
width += margin*2
widths.append(width)
all_expand_width = False
if child.sizing_mode in ('stretch_height', 'stretch_both', 'scale_height', 'scale_both'):
if smode in ('stretch_height', 'stretch_both', 'scale_height', 'scale_both'):
expand_height = True
else:
height = child.height or child.min_height
Expand Down
5 changes: 3 additions & 2 deletions panel/layout/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,13 @@ def _update_header(self, *events):

def _get_objects(self, model, old_objects, doc, root, comm=None):
ref = root.ref['id']
models, old_models = super()._get_objects(model, old_objects, doc, root, comm)
if ref in self._header_layout._models:
header = self._header_layout._models[ref][0]
old_models.append(header)
else:
header = self._header_layout._get_model(doc, root, model, comm)
objects = super()._get_objects(model, old_objects, doc, root, comm)
return [header]+objects
return [header]+models, old_models

def _compute_sizing_mode(self, children, props):
return super()._compute_sizing_mode(children[1:], props)
24 changes: 14 additions & 10 deletions panel/layout/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
model = self._bokeh_model()
root = root or model
self._models[root.ref['id']] = (model, parent)
objects = self._get_objects(model, [], doc, root, comm)
objects, _ = self._get_objects(model, [], doc, root, comm)
children = self._get_children(objects, self.nrows, self.ncols)
css_classes = self._compute_css_classes(children)
properties = {k: v for k, v in self._get_properties(doc).items() if k not in ('ncols', 'nrows')}
Expand All @@ -206,21 +206,24 @@ def _update_model(
old = events['objects'].old
else:
old = self.objects
objects = self._get_objects(model, old, doc, root, comm)
objects, old_models = self._get_objects(model, old, doc, root, comm)
children = self._get_children(objects, self.nrows, self.ncols)
msg[self._rename['objects']] = children
else:
old_models = None

with hold(doc):
msg = {k: v for k, v in msg.items() if k not in ('nrows', 'ncols')}
update = Panel._batch_update
Panel._batch_update = True
try:
super(Panel, self)._update_model(events, msg, root, model, doc, comm)
if update:
return
ref = root.ref['id']
if ref in state._views and preprocess:
state._views[ref][0]._preprocess(root)
with doc.models.freeze():
super(Panel, self)._update_model(events, msg, root, model, doc, comm)
if update:
return
ref = root.ref['id']
if ref in state._views and preprocess:
state._views[ref][0]._preprocess(root, self, old_models)
finally:
Panel._batch_update = update

Expand Down Expand Up @@ -315,7 +318,7 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
if old not in current_objects:
old._cleanup(root)

children = []
children, old_children = [], []
for i, ((y0, x0, y1, x1), obj) in enumerate(self.objects.items()):
x0 = 0 if x0 is None else x0
x1 = (self.ncols) if x1 is None else x1
Expand All @@ -341,6 +344,7 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):

if obj in old_objects:
child, _ = obj._models[root.ref['id']]
old_children.append(child)
else:
try:
child = obj._get_model(doc, root, model, comm)
Expand All @@ -352,7 +356,7 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
else:
child.update(**properties)
children.append((child, r, c, h, w))
return children
return children, old_children

def _compute_sizing_mode(self, children, props):
children = [child for (child, _, _, _, _) in children]
Expand Down
5 changes: 3 additions & 2 deletions panel/layout/tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
models and cleaning up any dropped objects.
"""
from ..pane.base import RerenderError, panel
new_models = []
new_models, old_models = [], []
if len(self._names) != len(self):
raise ValueError('Tab names do not match objects, ensure '
'that the Tabs.objects are not modified '
Expand Down Expand Up @@ -200,6 +200,7 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):

if prev_hidden and not hidden and pref in rendered:
child = rendered[pref]
old_models.append(child)
elif hidden:
child = BkSpacer(**{k: v for k, v in pane.param.values().items()
if k in Layoutable.param and v is not None and
Expand All @@ -215,4 +216,4 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
title=name, name=pane.name, child=child, closable=self.closable
)
new_models.append(panel)
return new_models
return new_models, old_models
28 changes: 21 additions & 7 deletions panel/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ def init(self) -> None:
"""
Registers the Callback
"""
if Callback._process_callbacks not in Viewable._preprocessing_hooks:
Viewable._preprocessing_hooks.append(Callback._process_callbacks)

source = self.source
if source is None:
return
Expand Down Expand Up @@ -193,13 +196,27 @@ def source(self) -> Reactive | None:
return self._source() if self._source else None

@classmethod
def _process_callbacks(cls, root_view: 'Viewable', root_model: BkModel):
def _process_callbacks(
cls, root_view: Viewable, root_model: BkModel, changed: Viewable | None = None, old_models=None
):
if not root_model:
return

linkable = (
root_view.select(Viewable) + list(root_model.select({'type' : BkModel})) # type: ignore
)
ref = root_model.ref['id']
if changed is not None:
inspect = root_view.select(Viewable)
if ref in changed._models:
inspect += changed._models[ref][0].select({'type' : BkModel})
targets = [link.target for links in cls.registry.values() for link in links if isinstance(link, Link)]
if not any(m in cls.registry or m in targets for m in inspect):
return

if root_view is changed:
linkable = inspect
else:
linkable = (
root_view.select(Viewable) + list(root_model.select({'type' : BkModel})) # type: ignore
)

if not linkable:
return
Expand Down Expand Up @@ -237,7 +254,6 @@ def _process_callbacks(cls, root_view: 'Viewable', root_model: BkModel):
for tgt in hv_objs:
arg_overrides[id(link)][k] = tgt

ref = root_model.ref['id']
for (link, src, tgt) in found:
cb = cls._callbacks[type(link)]
if ((src is None or ref not in getattr(src, '_models', [ref])) or
Expand Down Expand Up @@ -751,8 +767,6 @@ def _get_code(
Callback.register_callback(callback=JSCallbackGenerator)
Link.register_callback(callback=JSLinkCallbackGenerator)

Viewable._preprocessing_hooks.append(Callback._process_callbacks)

__all__ = (
"Callback",
"CallbackGenerator",
Expand Down
4 changes: 3 additions & 1 deletion panel/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,9 @@ def _process_param_change(self, msg: Dict[str, Any]) -> Dict[str, Any]:
properties['min_height'] = properties['height']
if 'stylesheets' in properties:
from .config import config
stylesheets = [loading_css(), f'{CDN_DIST}css/loading.css']
stylesheets = [loading_css(
config.loading_spinner, config.loading_color, config.loading_max_height
), f'{CDN_DIST}css/loading.css']
stylesheets += process_raw_css(config.raw_css)
stylesheets += config.css_files
stylesheets += [
Expand Down
4 changes: 3 additions & 1 deletion panel/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,9 @@ def resolve_resources(self, cdn: bool | Literal['auto'] = 'auto') -> ResourcesTy
name = clsname.lower()
dist_path = get_dist_path(cdn=cdn)

raw_css.extend(list(self.config.raw_css) + [loading_css()])
raw_css.extend(list(self.config.raw_css) + [loading_css(
config.loading_spinner, config.loading_color, config.loading_max_height
)])
for rname, res in self._design.resolve_resources(cdn).items():
if isinstance(res, dict):
resource_types[rname].update(res)
Expand Down
Loading
Loading