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

Throttle events in pyodide #3868

Merged
merged 1 commit into from Sep 21, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions panel/_templates/pyodide_worker.js
Expand Up @@ -37,14 +37,15 @@ self.onmessage = async (event) => {
from panel.io.state import state
from panel.io.pyodide import _link_docs_worker

_link_docs_worker(state.curdoc, sendPatch)
_link_docs_worker(state.curdoc, sendPatch, setter='js')
`)
} else if (event.data.type === 'patch') {
self.pyodide.runPythonAsync(`
import json

state.curdoc.apply_json_patch(json.loads('${event.data.patch}'))
state.curdoc.apply_json_patch(json.loads('${event.data.patch}'), setter='js')
`)
self.postMessage({type: 'idle'})
}
}

Expand Down
39 changes: 32 additions & 7 deletions panel/io/convert.py
Expand Up @@ -108,19 +108,44 @@ def _stdlibs():
PYODIDE_WORKER_SCRIPT = """
<script type="text/javascript">
const pyodideWorker = new Worker("./{{ name }}.js");
pyodideWorker.busy = false
pyodideWorker.queue = []

function send_change(jsdoc, event) {
if (event.setter_id != null)
if (event.setter_id != null && event.setter_id == 'py') {
return
} else if (pyodideWorker.busy && event.model && event.attr) {
let events = []
for (const old_event of pyodideWorker.queue) {
if (!(old_event.model === event.model && old_event.attr === event.attr)) {
events.push(old_event)
}
}
events.push(event)
pyodideWorker.queue = events
return
}
const patch = jsdoc.create_json_patch_string([event])
pyodideWorker.busy = true
pyodideWorker.postMessage({type: 'patch', patch: patch})
}

pyodideWorker.onmessage = async (event) => {
if (event.data.type === 'render') {
const docs_json = JSON.parse(event.data.docs_json)
const render_items = JSON.parse(event.data.render_items)
const root_ids = JSON.parse(event.data.root_ids)
const msg = event.data

if (msg.type === 'idle') {
if (pyodideWorker.queue.length) {
const patch = pyodideWorker.jsdoc.create_json_patch_string(pyodideWorker.queue)
pyodideWorker.busy = true
pyodideWorker.queue = []
pyodideWorker.postMessage({type: 'patch', patch: patch})
} else {
pyodideWorker.busy = false
}
} else if (msg.type === 'render') {
const docs_json = JSON.parse(msg.docs_json)
const render_items = JSON.parse(msg.render_items)
const root_ids = JSON.parse(msg.root_ids)

// Remap roots in message to element IDs
const root_els = document.getElementsByClassName('bk-root')
Expand Down Expand Up @@ -148,8 +173,8 @@ def _stdlibs():
pyodideWorker.jsdoc = jsdoc = views[0].model.document
jsdoc.on_change(send_change.bind(null, jsdoc), false)
pyodideWorker.postMessage({'type': 'rendered'})
} else if (event.data.type === 'patch') {
pyodideWorker.jsdoc.apply_json_patch(JSON.parse(event.data.patch), event.data.buffers, setter_id='js')
} else if (msg.type === 'patch') {
pyodideWorker.jsdoc.apply_json_patch(JSON.parse(msg.patch), msg.buffers, setter_id='py')
}
};
</script>
Expand Down
21 changes: 14 additions & 7 deletions panel/io/pyodide.py
Expand Up @@ -129,7 +129,7 @@ def _model_json(model: Model, target: str) -> Tuple[Document, str]:
version = __version__,
))

def _link_docs(pydoc: Document, jsdoc: Any) -> None:
def _link_docs(pydoc: Document, jsdoc: Any, setter: str | None =None) -> None:
"""
Links Python and JS documents in Pyodide ensuring taht messages
are passed between them.
Expand All @@ -140,29 +140,32 @@ def _link_docs(pydoc: Document, jsdoc: Any) -> None:
The Python Bokeh Document instance to sync.
jsdoc: Javascript Document
The Javascript Bokeh Document instance to sync.
setter: str
Setter ID used for suppressing events.
"""
def jssync(event):
if (getattr(event, 'setter_id', None) is not None):
if (event.setter_id is not None and event.setter_id == setter):
return
events = [event]
json_patch = jsdoc.create_json_patch_string(pyodide.ffi.to_js(events))
pydoc.apply_json_patch(json.loads(json_patch))
json_patch = jsdoc.create_json_patch_string(pyodide.ffi.to_js([event]))
pydoc.apply_json_patch(json.loads(json_patch), setter=setter)

jsdoc.on_change(pyodide.ffi.create_proxy(jssync), pyodide.ffi.to_js(False))

def pysync(event):
if event.setter is not None and event.setter == setter:
return
json_patch, buffers = process_document_events([event], use_buffers=True)
buffer_map = {}
for (ref, buffer) in buffers:
buffer_map[ref['id']] = pyodide.ffi.to_js(buffer).buffer
jsdoc.apply_json_patch(JSON.parse(json_patch), pyodide.ffi.to_js(buffer_map), setter_id='js')
jsdoc.apply_json_patch(JSON.parse(json_patch), pyodide.ffi.to_js(buffer_map), setter_id=setter)

pydoc.on_change(pysync)
pydoc.callbacks.trigger_json_event(
{'event_name': 'document_ready', 'event_values': {}
})

def _link_docs_worker(doc: Document, dispatch_fn: Any, msg_id: str | None = None):
def _link_docs_worker(doc: Document, dispatch_fn: Any, msg_id: str | None = None, setter: str | None = None):
"""
Links the Python document to a dispatch_fn which can be used to
sync messages between a WebWorker and the main thread in the
Expand All @@ -174,10 +177,14 @@ def _link_docs_worker(doc: Document, dispatch_fn: Any, msg_id: str | None = None
The document to dispatch messages from.
dispatch_fn: JS function
The Javascript function to dispatch messages to.
setter: str
Setter ID used for suppressing events.
msg_id: str | None
An optional message ID to pass through to the dispatch_fn.
"""
def pysync(event):
if setter is not None and event.setter == setter:
return
json_patch, buffers = process_document_events([event], use_buffers=True)
buffer_map = {}
for (ref, buffer) in buffers:
Expand Down