Repository: holoviz/holoviews

Issue:
Memory leak when producing Sankey plots with Bokeh
Thanks for contacting us! Please read and follow these instructions carefully, then delete this introductory text to keep your issue easy to read. Note that the issue tracker is NOT the place for usage questions and technical assistance; post those at [Discourse](https://discourse.holoviz.org) instead. Issues without the required information below may be closed immediately.


#### ALL software version info
OS Windows/Ubuntu
Python 3.7
bokeh 2.4.3
holoviews 1.14.9

#### Description of expected behavior and the observed behavior
We have got a standalone script that producing Sankey plots regularly. The Sankey plot are produced using Bokeh. Our scripts has is memory increasing continuously. We need to restart the script every 3 to 4 days before exhausting the memory of the machine.

According to my analysis, the memory leak is due to bokeh objects not being released. Bokeh objects are released if they are not used by Holoviews.

#### Complete, minimal, self-contained example code that reproduces the issue

```
import sys
import holoviews as hv
from collections import defaultdict
from bokeh.plotting import Figure
from bokeh.io import output_file
from bokeh.io import save
from pathlib import Path


DEFAULT_PLOT_WIDTH = 1200  # pixels
DEFAULT_PLOT_HEIGHT = 700  # pixels
TITLE_FONT_SIZE = "16pt"
LABEL_FONT_SIZE = "10pt"


def plot_port_targets(
        port_targets: dict,
        plot_width: int = DEFAULT_PLOT_WIDTH,
        plot_height: int = DEFAULT_PLOT_HEIGHT) -> Figure:
    hv.extension("bokeh")
    train_counts = defaultdict(int)
    for mine_name, port_names in port_targets.items():
        for port_name in port_names:
            train_counts[(mine_name, port_name)] += 1

    sankey = hv.Sankey(train_counts)
    sankey.opts(width=plot_width, height=plot_height)
    return hv.render(sankey)


def generate_html_file():
    output_file_path = Path('holoviews_memory_leak.html')
    planned_port_targets = {
        "Mine 1": [
            "Port 1",
            "Port 1",
            "Port 2",
            "Port 1",
            "Port 1",
            "Port 2",
            "Port 2",
            "Port 1"
        ],
        "Mine 2": [
            "Port 2",
            "Port 2",
            "Port 2"
        ],
        "Mine 3": [
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2"
        ],
        "Mine 4": [
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 1",
            "Port 2",
            "Port 1",
            "Port 2",
            "Port 2"
        ],
        "Mine 5": [
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 1",
            "Port 2",
            "Port 2"
        ],
        "Mine 6": [
            "Port 2",
            "Port 2"
        ],
        "Mine 7": [
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2"
        ],
        "Mine 8": [
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2",
            "Port 2"
        ]
    }

    planned_port_targets_plot = plot_port_targets(
        port_targets=planned_port_targets
    )
    output_file(output_file_path, title="Train Targets Visualisations", mode="inline")
    save(planned_port_targets_plot)


def main(argv):
    input('Press ENTER to start')
    for i in range(100):
        print('.', end='')
        generate_html_file()
    print('')
    input('Press ENTER to continue')
    for i in range(1000):
        if i % 100 or i == 0:
            print('.', end='')
        else:
            print('')
            print('.', end='')
        generate_html_file()
    print('')
    input('Press ENTER to continue')
    generate_html_file()
    input('Press ENTER to end')


if __name__ == '__main__':
    main(sys.argv)
```

#### Stack traceback and/or browser JavaScript console output

#### Screenshots or screencasts of the bug in action
![Uploading holoview memory leak.png…]()




Discussion:
I can recreate the problem and I think the culprit is related to `hv.render`. If I modify the original code to the following and run it with `memray run file.py` I get the following:

<details>
  <summary>Code </summary>

``` python
from collections import defaultdict

import holoviews as hv
from tqdm import tqdm


hv.extension("bokeh")


def main():
    port_targets = {
        1: [1, 1, 2, 1, 1, 2, 2, 1],
        2: [2, 2, 2],
        3: [2, 2, 2, 2, 2],
        4: [2, 2, 2, 2, 2, 1, 2, 1, 2, 2],
        5: [2, 2, 2, 1, 2, 2],
        6: [2, 2],
        7: [2, 2, 2, 2, 2],
        8: [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
    }

    train_counts = defaultdict(int)
    for mine_name, port_names in port_targets.items():
        for port_name in port_names:
            train_counts[(f"Mine {mine_name}", f"Port {port_name}")] += 1


    sankey = hv.Sankey(train_counts)
    hv.render(sankey)


if __name__ == "__main__":
    for _ in tqdm(range(1000)):
        main()

```

</details>
 
With `hv.render(sankey)`:

![newplot](https://user-images.githubusercontent.com/19758978/174764724-7ba987e9-280b-4902-8aa7-9798ea0ed65c.png)


Without `hv.render(sankey)`:

![newplot(1)](https://user-images.githubusercontent.com/19758978/174764826-a113b16f-cb23-4133-8719-5f085db196d6.png)


I then tried a simple Curve plot and I could see the same problem: 

<details>
  <summary>Code </summary>

``` python
import holoviews as hv
from tqdm import tqdm


hv.extension("bokeh")


def main():
    curve = hv.Curve((range(10_000), range(10_000)))
    hv.render(curve)


if __name__ == "__main__":
    for _ in tqdm(range(1000)):
        main()

```
</details>

 
With `hv.render(curve)`:
![newplot(3)](https://user-images.githubusercontent.com/19758978/174765235-4e54caa4-9113-4e9a-992b-ef1a80ccb4ec.png)


Without `hv.render(curve)`:
![newplot(4)](https://user-images.githubusercontent.com/19758978/174765471-02b4a460-744d-4d9e-8a0a-696fdd8c715d.png)



Resolving patch:
diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py
--- a/holoviews/plotting/renderer.py
+++ b/holoviews/plotting/renderer.py
@@ -271,7 +271,7 @@ def get_plot_state(self_or_cls, obj, renderer=None, **kwargs):
         Given a HoloViews Viewable return a corresponding plot state.
         """
         if not isinstance(obj, Plot):
-            obj = self_or_cls.get_plot(obj, renderer, **kwargs)
+            obj = self_or_cls.get_plot(obj=obj, renderer=renderer, **kwargs)
         return obj.state
 
     def _validate(self, obj, fmt, **kwargs):
diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py
--- a/holoviews/util/__init__.py
+++ b/holoviews/util/__init__.py
@@ -804,9 +804,10 @@ def render(obj, backend=None, **kwargs):
     renderer_obj = renderer(backend)
     if kwargs:
         renderer_obj = renderer_obj.instance(**kwargs)
-    plot = renderer_obj.get_plot(obj)
-    if backend == 'matplotlib' and len(plot) > 1:
-        return plot.anim(fps=renderer_obj.fps)
+    if backend == 'matplotlib':
+        plot = renderer_obj.get_plot(obj)
+        if len(plot) > 1:
+            return plot.anim(fps=renderer_obj.fps)
     return renderer_obj.get_plot_state(obj)
 
 


Test patch: