Repository: bokeh/bokeh

Issue:
Prove of concept: Caching of nodeJS compilation on model bundling
Dear Bokeh developers!

A while ago we shortly discussed on Gitter that in Bokeh server the compile time to generate the custom model JS inline is slow and should be cached.
Here we present a simple prove of concept for a caching of the nodeJS compilation that holds even when more than one disjunct models were delivered by a single bokeh server instance.

In bokeh/bokeh/util/compiler.py
```
def calc_cache_key():
    """There is no metadata than the Model classes,
        so this is the only base to generate a cache  key.
       We build the model keys from the list of model.full_name(s).
       This is ineffective but possibly a better solution can be found found.
       It is still at least an order of magnitude faster faster than the nodeJS :-)
    """

    models = Model.model_class_reverse_map.values()
    custom_model_names = ""

    for cls in models:
        impl = getattr(cls, "__implementation__", None)

        if impl is not None:
            model = CustomModel(cls)
            custom_model_names += model.full_name

    key = hashlib.sha256(custom_model_names.encode('utf-8')).hexdigest()
    return key

def bundle_all_models():
    if not hasattr(Model, '__cache'):
        Model.__cache = {}
    key = calc_cache_key()
    if key in Model.__cache:
        return Model.__cache[key]
    else:
        Model.__cache[key] = bundle_models(Model.model_class_reverse_map.values()) or ""
        return Model.__cache[key]
    if not Model.__cache[key]: #this is probably overkill
        Model.__cache[key] = bundle_models(Model.model_class_reverse_map.values()) or ""
    return Model.__cache[key]

#    return bundle_models(Model.model_class_reverse_map.values()) or ""
```
Cheers,
Volker



Discussion:

Resolving patch:
diff --git a/bokeh/sphinxext/bokeh_plot.py b/bokeh/sphinxext/bokeh_plot.py
--- a/bokeh/sphinxext/bokeh_plot.py
+++ b/bokeh/sphinxext/bokeh_plot.py
@@ -265,7 +265,7 @@ def run(self):
         return result
 
 def env_before_read_docs(app, env, docnames):
-    docnames.sort(key=lambda x: 2 if "extension" in x else 0 if "examples" in x else 1)
+    docnames.sort(key=lambda x: 0 if "examples" in x else 1)
     for name in [x for x in docnames if env.doc2path(x).endswith(".py")]:
         if not name.startswith(tuple(env.app.config.bokeh_plot_pyfile_include_dirs)):
             env.found_docs.remove(name)
diff --git a/bokeh/util/compiler.py b/bokeh/util/compiler.py
--- a/bokeh/util/compiler.py
+++ b/bokeh/util/compiler.py
@@ -426,5 +426,35 @@ def resolve_deps(deps, root):
     content = _plugin_template % dict(prelude=_plugin_prelude, exports=exports, modules=modules)
     return _plugin_umd % dict(content=content)
 
+def calc_cache_key():
+    ''' Generate a key to cache a custom extension implementation with.
+
+    There is no metadata other than the Model classes, so this is the only
+    base to generate a cache key.
+
+    We build the model keys from the list of ``model.full_name``. This is
+    not ideal but possibly a better solution can be found found later.
+
+    '''
+
+    models = Model.model_class_reverse_map.values()
+    custom_model_names = ""
+
+    for cls in models:
+        impl = getattr(cls, "__implementation__", None)
+
+        if impl is not None:
+            model = CustomModel(cls)
+            custom_model_names += model.full_name
+
+    key = hashlib.sha256(custom_model_names.encode('utf-8')).hexdigest()
+    return key
+
+_bundle_cache = {}
+
 def bundle_all_models():
-    return bundle_models(Model.model_class_reverse_map.values()) or ""
+    key = calc_cache_key()
+    bundle = _bundle_cache.get(key, None)
+    if bundle is None:
+        _bundle_cache[key] = bundle = bundle_models(Model.model_class_reverse_map.values()) or ""
+    return bundle


Test patch: