Skip to content

Commit 0210f79

Browse files
committed
fix client memory leak
1 parent 300d141 commit 0210f79

File tree

4 files changed

+52
-17
lines changed

4 files changed

+52
-17
lines changed

src/reactpy_django/components.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from uuid import uuid4
88
from warnings import warn
99

10-
import orjson
1110
from django.contrib.staticfiles.finders import find
1211
from django.core.cache import caches
1312
from django.http import HttpRequest
@@ -19,7 +18,6 @@
1918
from reactpy_django.exceptions import ViewNotRegisteredError
2019
from reactpy_django.html import pyscript
2120
from reactpy_django.utils import (
22-
extend_pyscript_config,
2321
generate_obj_name,
2422
import_module,
2523
render_pyscript_template,
@@ -324,13 +322,14 @@ def _python_to_pyscript(
324322
# FIXME: This is needed to properly re-render PyScript instances such as
325323
# when a component is re-rendered due to WebSocket disconnection.
326324
# There may be a better way to do this in the future.
327-
# While this solution allows re-creating PyScript components, it also
328-
# results in a browser memory leak. It currently unclear how to properly
329-
# clean up unused code (user_workspace_UUID) from PyScript.
330325
set_rendered(True)
331326
return None
332327

333328
return html.div(
334-
html.div((extra_props or {}) | {"id": f"pyscript-{uuid}"}, initial),
329+
html.div(
330+
(extra_props or {})
331+
| {"id": f"pyscript-{uuid}", "className": "pyscript", "data-uuid": uuid},
332+
initial,
333+
),
335334
pyscript({"async": ""}, executor),
336335
)
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
"""Code within this module is designed to be run directly by PyScript, and is not
2-
intended to be run in a Django environment.
3-
4-
Our template tag performs string substitutions to turn this file into valid PyScript."""
5-
1+
# pylint: disable=used-before-assignment
62
from typing import TYPE_CHECKING
73

84
if TYPE_CHECKING:
5+
import asyncio
6+
97
from reactpy_django.pyscript.layout_manager import ReactPyLayoutManager
108

119

1210
# User component is inserted below by regex replacement
1311
def user_workspace_UUID():
1412
"""Encapsulate the user's code with a completely unique function (workspace)
15-
to prevent overlapping imports and variable names between different components."""
13+
to prevent overlapping imports and variable names between different components.
14+
15+
This code is designed to be run directly by PyScript, and is not intended to be run
16+
in a standard Python environment.
17+
18+
Our template tag performs string substitutions to turn this file into valid PyScript.
19+
"""
1620

1721
def root(): ...
1822

@@ -21,4 +25,4 @@ def root(): ...
2125

2226
# PyScript allows top-level await, which allows us to not throw errors on components
2327
# that terminate early (such as hook-less components)
24-
await ReactPyLayoutManager("UUID").run(user_workspace_UUID) # noqa: F704
28+
task_UUID = asyncio.create_task(ReactPyLayoutManager("UUID").run(user_workspace_UUID))

src/reactpy_django/pyscript/layout_manager.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99

1010
class ReactPyLayoutManager:
1111
"""Encapsulate the entire layout manager with a class to prevent overlapping
12-
variable names between user code."""
12+
variable names between user code.
13+
14+
This code is designed to be run directly by PyScript, and is not intended to be run
15+
in a standard Python environment.
16+
"""
1317

1418
def __init__(self, uuid):
1519
self.uuid = uuid
@@ -68,8 +72,38 @@ def event_handler(*args):
6872
event_name = event_name.lstrip("on_").lower().replace("_", "")
6973
add_event_listener(element, event_name, event_handler)
7074

75+
@staticmethod
76+
def delete_old_workspaces():
77+
dom_workspaces = js.document.querySelectorAll(".pyscript")
78+
dom_uuids = {element.dataset.uuid for element in dom_workspaces}
79+
python_uuids = {
80+
value.split("_")[-1]
81+
for value in globals()
82+
if value.startswith("user_workspace_")
83+
}
84+
85+
# Delete the workspace if it exists at the moment when we check
86+
for uuid in python_uuids - dom_uuids:
87+
task_name = f"task_{uuid}"
88+
if task_name in globals():
89+
task: asyncio.Task = globals()[task_name]
90+
task.cancel()
91+
del globals()[task_name]
92+
else:
93+
print(f"Warning: Could not auto delete PyScript task {task_name}")
94+
95+
workspace_name = f"user_workspace_{uuid}"
96+
if workspace_name in globals():
97+
del globals()[workspace_name]
98+
else:
99+
print(
100+
f"Warning: Could not auto delete PyScript workspace {workspace_name}"
101+
)
102+
71103
async def run(self, workspace_function: Coroutine):
104+
self.delete_old_workspaces()
72105
root_model = {}
106+
73107
async with Layout(workspace_function()) as layout:
74108
while True:
75109
update = await layout.render()
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
{% if reactpy_class %}<div id="pyscript-{{pyscript_uuid}}" class="{{pyscript_class}}">{{pyscript_initial_html}}</div>
2-
{% endif %}
3-
{% if not reactpy_class %}<div id="pyscript-{{pyscript_uuid}}">{{pyscript_initial_html}}</div>{% endif %}
1+
<div id="pyscript-{{pyscript_uuid}}" class="pyscript" data-uuid="{{pyscript_uuid}}">{{pyscript_initial_html}}</div>
42
<py-script async>{{pyscript_executor}}</py-script>

0 commit comments

Comments
 (0)