Repository: bokeh/bokeh

Issue:
embed components performance
bokeh==0.12.5dev13-4-gd90175c-dirty
python==3.5

If a model or a list of models are passed to `embed.components`, then `.references()` is run on each of the models to check if there is an associated document. If there isn't then a new document is created and in add_root, all `.references()` is called again on all of the models. 

Performance could be improved by only calling `.references()` once on each model

Here is a minimal example with profiling.

```python
from bokeh.plotting import figure
from bokeh.embed import components

x1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
y1 = [0, 8, 2, 4, 6, 9, 5, 6, 25, 28, 4]

p1 = figure()
p1.scatter(x1, y1)

script, div = components(p1)
```
snakeviz profiling:
<img width="1127" alt="screen shot 2017-02-22 at 10 26 12 am" src="https://cloud.githubusercontent.com/assets/4806877/23218561/88a1c954-f8ea-11e6-90de-28e57e476e52.png">

raw text:
15421 function calls (15293 primitive calls) in 0.012 seconds

Ordered by: internal time

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
3125    0.001    0.000    0.001    0.000 {built-in method builtins.isinstance}
175    0.001    0.000    0.005    0.000 bases.py:259(prepare_value)
264    0.001    0.000    0.001    0.000 bases.py:354(validate)
1041    0.000    0.000    0.000    0.000 {built-in method builtins.hasattr}
370/322    0.000    0.000    0.001    0.000 model.py:698(_visit_value_and_its_immediate_references)
  3    0.000    0.000    0.001    0.000 encoder.py:203(iterencode)
160/150    0.000    0.000    0.003    0.000 properties.py:669(validate)
441    0.000    0.000    0.007    0.000 descriptors.py:615(_get)
458    0.000    0.000    0.007    0.000 {built-in method builtins.getattr}
175    0.000    0.000    0.006    0.000 bases.py:135(themed_default)
 50    0.000    0.000    0.008    0.000 model.py:685(_visit_immediate_value_references)
190    0.000    0.000    0.001    0.000 {method 'join' of 'str' objects}
300    0.000    0.000    0.001    0.000 <frozen importlib._bootstrap>:996(_handle_fromlist)
 94    0.000    0.000    0.001    0.000 properties.py:1246(validate)
 25    0.000    0.000    0.001    0.000 has_props.py:515(query_properties_with_values)
334/304    0.000    0.000    0.003    0.000 bases.py:227(is_valid)
537    0.000    0.000    0.007    0.000 descriptors.py:414(__get__)
 39    0.000    0.000    0.000    0.000 json_encoder.py:118(default)
450/430    0.000    0.000    0.003    0.000 properties.py:672(<genexpr>)
175    0.000    0.000    0.006    0.000 descriptors.py:521(instance_default)
175    0.000    0.000    0.006    0.000 descriptors.py:643(_get_default)
168    0.000    0.000    0.000    0.000 properties.py:419(__str__)
175    0.000    0.000    0.000    0.000 copy.py:67(copy)
150    0.000    0.000    0.000    0.000 properties.py:660(transform)
175    0.000    0.000    0.000    0.000 bases.py:114(_copy_default)
 98    0.000    0.000    0.000    0.000 string.py:38(nice_join)
 50    0.000    0.000    0.000    0.000 has_props.py:569(apply_theme)
175    0.000    0.000    0.000    0.000 has_props.py:202(accumulate_dict_from_superclasses)
 84    0.000    0.000    0.001    0.000 properties.py:644(__str__)
175    0.000    0.000    0.000    0.000 bases.py:244(_wrap_container)
 84    0.000    0.000    0.001    0.000 properties.py:1231(__str__)
175    0.000    0.000    0.000    0.000 bases.py:103(_has_stable_default)
  2    0.000    0.000    0.005    0.002 document.py:1069(_recompute_all_models)
141    0.000    0.000    0.000    0.000 bases.py:125(_raw_default)
  1    0.000    0.000    0.000    0.000 decoder.py:345(raw_decode)
168    0.000    0.000    0.000    0.000 properties.py:426(instance_type)
  2    0.000    0.000    0.008    0.004 model.py:25(collect_models)
370    0.000    0.000    0.000    0.000 properties.py:647(type_params)
 81    0.000    0.000    0.000    0.000 descriptors.py:239(serializable_value)
136/126    0.000    0.000    0.003    0.000 {built-in method builtins.any}
175    0.000    0.000    0.000    0.000 has_props.py:506(_overridden_defaults)
518    0.000    0.000    0.000    0.000 bases.py:210(validate)
 99    0.000    0.000    0.000    0.000 _weakrefset.py:70(__contains__)
200    0.000    0.000    0.000    0.000 has_props.py:554(themed_values)
 95    0.000    0.000    0.000    0.000 abc.py:178(__instancecheck__)
 50    0.000    0.000    0.000    0.000 theme.py:95(apply_to_model)
 25    0.000    0.000    0.001    0.000 model.py:541(_to_json_like)
252    0.000    0.000    0.000    0.000 bases.py:64(__str__)
 50    0.000    0.000    0.000    0.000 has_props.py:179(accumulate_from_superclasses)
 96    0.000    0.000    0.000    0.000 bases.py:149(serialized)
 96    0.000    0.000    0.000    0.000 descriptors.py:603(serialized)
 74    0.000    0.000    0.000    0.000 six.py:580(iteritems)
 50    0.000    0.000    0.000    0.000 has_props.py:241(__setattr__)
 44    0.000    0.000    0.001    0.000 properties.py:1740(prepare_value)
 98    0.000    0.000    0.000    0.000 string.py:54(<listcomp>)
 80    0.000    0.000    0.000    0.000 model.py:46(queue_one)
 96    0.000    0.000    0.000    0.000 has_props.py:504(<lambda>)
 98    0.000    0.000    0.000    0.000 bases.py:359(<listcomp>)
  1    0.000    0.000    0.012    0.012 embed.py:52(components)
 64    0.000    0.000    0.000    0.000 model.py:300(ref)
 25    0.000    0.000    0.000    0.000 model.py:519(_attach_document)
  1    0.000    0.000    0.001    0.001 document.py:1093(_references_json)
 10    0.000    0.000    0.000    0.000 os.py:720(__getitem__)
  2    0.000    0.000    0.000    0.000 document.py:1169(_with_self_as_curdoc)
  1    0.000    0.000    0.004    0.004 model.py:720(__init__)
  2    0.000    0.000    0.000    0.000 serialization.py:47(make_id)
  1    0.000    0.000    0.000    0.000 serialization.py:229(transform_column_source_data)
  2    0.000    0.000    0.000    0.000 uuid.py:106(__init__)
175    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
  3    0.000    0.000    0.000    0.000 runtime.py:115(__init__)
 96    0.000    0.000    0.000    0.000 has_props.py:394(lookup)
 50    0.000    0.000    0.000    0.000 has_props.py:408(properties_with_refs)
 25    0.000    0.000    0.000    0.000 model.py:531(_detach_document)
  1    0.000    0.000    0.004    0.004 model.py:652(_find_some_document)
  1    0.000    0.000    0.002    0.002 embed.py:531(_standalone_docs_json_and_render_items)
  1    0.000    0.000    0.005    0.005 document.py:233(add_root)
 50    0.000    0.000    0.000    0.000 theme.py:80(_for_class)
 10    0.000    0.000    0.000    0.000 settings.py:26(_get)
  2    0.000    0.000    0.000    0.000 {built-in method posix.urandom}
  1    0.000    0.000    0.012    0.012 {built-in method builtins.exec}
159    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
  1    0.000    0.000    0.012    0.012 <string>:2(<module>)
  1    0.000    0.000    0.002    0.002 document.py:707(to_json)
  9    0.000    0.000    0.000    0.000 properties.py:1465(to_serializable)
  2    0.000    0.000    0.000    0.000 serialization.py:194(traverse_data)
 25    0.000    0.000    0.001    0.000 has_props.py:483(properties_with_values)
 58    0.000    0.000    0.000    0.000 {method 'pop' of 'list' objects}
10/5    0.000    0.000    0.000    0.000 settings.py:42(_get_bool)
154    0.000    0.000    0.000    0.000 {method 'items' of 'dict' objects}
 10    0.000    0.000    0.000    0.000 _collections_abc.py:594(get)
 15    0.000    0.000    0.000    0.000 descriptors.py:846(serializable_value)
  2    0.000    0.000    0.000    0.000 uuid.py:601(uuid4)
165    0.000    0.000    0.000    0.000 copy.py:110(_copy_immutable)
  3    0.000    0.000    0.001    0.000 json_encoder.py:151(serialize_json)
 10    0.000    0.000    0.000    0.000 containers.py:92(__init__)
  1    0.000    0.000    0.000    0.000 document.py:85(__init__)
  1    0.000    0.000    0.000    0.000 embed.py:442(_script_for_render_items)
  2    0.000    0.000    0.000    0.000 document.py:1139(_trigger_on_change)
  2    0.000    0.000    0.000    0.000 embed.py:476(_check_models)
175    0.000    0.000    0.000    0.000 bases.py:198(transform)
 20    0.000    0.000    0.000    0.000 properties.py:1251(<genexpr>)
207    0.000    0.000    0.000    0.000 {built-in method builtins.len}
 10    0.000    0.000    0.000    0.000 os.py:796(encode)
  2    0.000    0.000    0.008    0.004 model.py:393(references)
  3    0.000    0.000    0.000    0.000 runtime.py:55(new_context)
  6    0.000    0.000    0.000    0.000 properties.py:1721(to_serializable)
 10    0.000    0.000    0.000    0.000 properties.py:1551(prepare_value)
 50    0.000    0.000    0.000    0.000 {method 'add' of 'set' objects}
  3    0.000    0.000    0.000    0.000 environment.py:974(render)
 10    0.000    0.000    0.000    0.000 containers.py:260(__init__)
 14    0.000    0.000    0.000    0.000 {built-in method builtins.all}
  6    0.000    0.000    0.000    0.000 properties.py:1705(isconst)
  2    0.000    0.000    0.000    0.000 io.py:245(curdoc)
  2    0.000    0.000    0.000    0.000 enums.py:77(__contains__)
  2    0.000    0.000    0.000    0.000 {built-in method from_bytes}
 51    0.000    0.000    0.000    0.000 {method 'startswith' of 'str' objects}
  1    0.000    0.000    0.002    0.002 document.py:720(to_json_string)
  3    0.000    0.000    0.001    0.000 __init__.py:182(dumps)
 51    0.000    0.000    0.000    0.000 {method 'union' of 'set' objects}
  1    0.000    0.000    0.000    0.000 document.py:593(remove_root)
 78    0.000    0.000    0.000    0.000 {built-in method builtins.iter}
  3    0.000    0.000    0.000    0.000 environment.py:1015(new_context)
  2    0.000    0.000    0.000    0.000 embed.py:32(_indent)
 80    0.000    0.000    0.000    0.000 bases.py:192(serialize_value)
  3    0.000    0.000    0.000    0.000 nodes.py:81(__init__)
  2    0.000    0.000    0.000    0.000 embed.py:33(<listcomp>)
  1    0.000    0.000    0.000    0.000 decoder.py:334(decode)
  1    0.000    0.000    0.000    0.000 events.py:125(__init__)
  3    0.000    0.000    0.001    0.000 encoder.py:181(encode)
 10    0.000    0.000    0.000    0.000 copy.py:125(_copy_with_constructor)
  3    0.000    0.000    0.000    0.000 encoder.py:104(__init__)
  2    0.000    0.000    0.000    0.000 uuid.py:226(__str__)
  1    0.000    0.000    0.005    0.005 model.py:742(__enter__)
  2    0.000    0.000    0.005    0.002 document.py:1061(_pop_all_models_freeze)
  3    0.000    0.000    0.000    0.000 doc_js.js:5(root)
  5    0.000    0.000    0.000    0.000 settings.py:29(_is_dev)
  4    0.000    0.000    0.000    0.000 properties.py:1642(prepare_value)
  3    0.000    0.000    0.000    0.000 datatypes.py:12(__init__)
10/5    0.000    0.000    0.000    0.000 settings.py:33(_dev_or_default)
  2    0.000    0.000    0.000    0.000 {method 'split' of 'str' objects}
  2    0.000    0.000    0.000    0.000 script_tag.html:5(root)
 76    0.000    0.000    0.000    0.000 {method 'keys' of 'dict' objects}
  1    0.000    0.000    0.000    0.000 properties.py:1293(serialize_value)
 25    0.000    0.000    0.000    0.000 document.py:149(theme)
  1    0.000    0.000    0.000    0.000 embed.py:41(_wrap_in_onload)
  4    0.000    0.000    0.000    0.000 io.py:223(set_curdoc)
  1    0.000    0.000    0.000    0.000 embed.py:35(_wrap_in_safely)
 30    0.000    0.000    0.000    0.000 model.py:293(document)
  1    0.000    0.000    0.000    0.000 model.py:738(__exit__)
  2    0.000    0.000    0.000    0.000 settings.py:133(simple_ids)
  2    0.000    0.000    0.000    0.000 document.py:1144(invoke_callbacks)
  4    0.000    0.000    0.000    0.000 properties.py:1625(prepare_value)
  1    0.000    0.000    0.000    0.000 string.py:8(encode_utf8)
 10    0.000    0.000    0.000    0.000 {method 'encode' of 'str' objects}
 10    0.000    0.000    0.000    0.000 settings.py:22(_environ)
  4    0.000    0.000    0.000    0.000 serialization.py:211(<genexpr>)
  2    0.000    0.000    0.000    0.000 plot_div.html:5(root)
  1    0.000    0.000    0.000    0.000 __init__.py:271(loads)
  2    0.000    0.000    0.000    0.000 {method 'match' of '_sre.SRE_Pattern' objects}
  2    0.000    0.000    0.000    0.000 events.py:11(__init__)
  5    0.000    0.000    0.000    0.000 runtime.py:149(resolve)
  3    0.000    0.000    0.000    0.000 settings.py:127(pretty)
  2    0.000    0.000    0.000    0.000 state.py:46(document)
  1    0.000    0.000    0.000    0.000 six.py:574(iterkeys)
  4    0.000    0.000    0.000    0.000 embed.py:484(<genexpr>)
  1    0.000    0.000    0.000    0.000 embed.py:509(_div_for_render_item)
  2    0.000    0.000    0.000    0.000 embed.py:143(<genexpr>)
  2    0.000    0.000    0.000    0.000 document.py:1055(_push_all_models_freeze)
  3    0.000    0.000    0.000    0.000 runtime.py:126(<genexpr>)
  1    0.000    0.000    0.000    0.000 events.py:137(__init__)
  2    0.000    0.000    0.000    0.000 {method 'pop' of 'dict' objects}
  3    0.000    0.000    0.000    0.000 {built-in method builtins.callable}
  2    0.000    0.000    0.000    0.000 {method 'lower' of 'str' objects}
  5    0.000    0.000    0.000    0.000 {method 'values' of 'dict' objects}
  2    0.000    0.000    0.000    0.000 document.py:107(roots)
  2    0.000    0.000    0.000    0.000 {method 'count' of 'list' objects}
  1    0.000    0.000    0.000    0.000 document.py:174(title)
  3    0.000    0.000    0.000    0.000 _compat.py:29(<lambda>)
  1    0.000    0.000    0.000    0.000 {method 'remove' of 'list' objects}
  1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
  4    0.000    0.000    0.000    0.000 state.py:54(document)
  2    0.000    0.000    0.000    0.000 {method 'end' of '_sre.SRE_Match' objects}




Discussion:

Resolving patch:
diff --git a/bokeh/embed.py b/bokeh/embed.py
--- a/bokeh/embed.py
+++ b/bokeh/embed.py
@@ -15,6 +15,7 @@
 
 from collections import Sequence
 from warnings import warn
+import re
 
 from six import string_types
 
@@ -49,6 +50,30 @@ def _wrap_in_onload(code):
 })();
 """ % dict(code=_indent(code, 4))
 
+
+def _add_doc_to_models(doc, models):
+    models_to_dedoc = []
+    for model in models:
+        if isinstance(model, Model):
+            if model.document is None:
+                try:
+                    doc.add_root(model)
+                    models_to_dedoc.append(model)
+                except RuntimeError as e:
+                    child = re.search('\((.*)\)', str(e)).group(0)
+                    msg = ('Sub-model {0} of the root model {1} is already owned '
+                           'by another document (Models must be owned by only a '
+                           'single document). This may indicate a usage '
+                           'error.'.format(child, model))
+                    raise RuntimeError(msg)
+    return models_to_dedoc
+
+
+def _remove_doc_from_models(doc, models):
+    for model in models:
+        doc.remove_root(model)
+
+
 def components(models, wrap_script=True, wrap_plot_info=True):
     '''
     Return HTML components to embed a Bokeh plot. The data for the plot is
@@ -131,10 +156,27 @@ def components(models, wrap_script=True, wrap_plot_info=True):
             values.append(models[k])
         models = values
 
-    # 2) Do our rendering
+    # 2) Append models to one document. Either pre-existing or new.
+    existing_docs = set(m if isinstance(m, Document) else m.document for m in models)
+    existing_docs.discard(None)
 
-    with _ModelInDocument(models):
-        (docs_json, render_items) = _standalone_docs_json_and_render_items(models)
+    if len(existing_docs) == 0:
+        # no existing docs, make a new one
+        doc = Document()
+    elif len(existing_docs) == 1:
+        # all existing docs are the same, use that one
+        doc = existing_docs.pop()
+    else:
+        # conflicting/multiple docs, raise an error
+        msg = ('Multiple items in models conatain documents or are '
+               'themselves documents. (Models must be owned by only a '
+               'single document). This may indicate a usage error.')
+        raise RuntimeError(msg)
+
+    models_to_dedoc = _add_doc_to_models(doc, models)
+
+    # 3) Do our rendering
+    (docs_json, render_items) = _standalone_docs_json_and_render_items(models)
 
     script = _script_for_render_items(docs_json, render_items, websocket_url=None, wrap_script=wrap_script)
     script = encode_utf8(script)
@@ -144,7 +186,10 @@ def components(models, wrap_script=True, wrap_plot_info=True):
     else:
         results = render_items
 
-    # 3) convert back to the input shape
+    # 4) Clean up the doc
+    _remove_doc_from_models(doc, models_to_dedoc)
+
+    # 5) convert back to the input shape
 
     if was_single_object:
         return script, results[0]


Test patch: