Skip to content

Use real references to Graphics, proper garbage collection in ipython and jupyter #546

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

Merged
merged 9 commits into from
Jul 10, 2024
1 change: 0 additions & 1 deletion docs/source/api/layouts/subplot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ Methods
Subplot.clear
Subplot.delete_graphic
Subplot.get_rect
Subplot.get_refcounts
Subplot.insert_graphic
Subplot.map_screen_to_world
Subplot.remove_animation
Expand Down
109 changes: 91 additions & 18 deletions examples/notebooks/test_gc.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"metadata": {},
"outputs": [],
"source": [
"import weakref\n",
"import fastplotlib as fpl\n",
"import numpy as np\n",
"import pytest"
Expand All @@ -23,7 +24,7 @@
" for i in range(len(plot_objects)):\n",
" with pytest.raises(ReferenceError) as failure:\n",
" plot_objects[i]\n",
" pytest.fail(f\"GC failed for object: {objects[i]}\")"
" pytest.fail(f\"GC failed for object: {plot_objects[i]} of type: {plot_objects[i].__class__.__name__}\")"
]
},
{
Expand All @@ -49,7 +50,15 @@
"\n",
"line_collection_data = [points_data[:, 1].copy() for i in range(10)]\n",
"\n",
"img_data = np.random.rand(2_000, 2_000)"
"img_data = np.random.rand(1_000, 1_000)"
]
},
{
"cell_type": "markdown",
"id": "2a8a92e1-70bc-41b5-9ad8-b86dab6e74eb",
"metadata": {},
"source": [
"# Make references to each graphic"
]
},
{
Expand All @@ -76,50 +85,114 @@
"linear_region_sel_img = image.add_linear_region_selector(name=\"image_linear_region_sel\")"
]
},
{
"cell_type": "markdown",
"id": "d691c3c6-0d82-4aa8-90e9-165efffda369",
"metadata": {},
"source": [
"# Add event handlers"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bb2083c1-f6b7-417c-86b8-9980819917db",
"id": "64198fd0-edd4-4ba1-8082-a65d57b83881",
"metadata": {},
"outputs": [],
"source": [
"def feature_changed_handler(ev):\n",
" pass\n",
"\n",
"\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4a86c37b-41ce-4b50-af43-ef61d36b7d81",
"metadata": {},
"outputs": [],
"source": [
"objects = list()\n",
"weakrefs = list() # used to make sure the real objs are garbage collected\n",
"for subplot in fig:\n",
" objects += subplot.objects\n",
"\n",
" for obj in subplot.objects:\n",
" objects.append(obj)\n",
" weakrefs.append(weakref.proxy(obj))\n",
"\n",
"for g in objects:\n",
" for feature in g._features:\n",
" # if isinstance(g, fpl.LineCollection):?\n",
" # continue # skip collections for now\n",
" \n",
" g.add_event_handler(feature_changed_handler, feature)\n",
"\n",
" g.add_event_handler(feature_changed_handler, feature)"
]
},
{
"cell_type": "markdown",
"id": "ecd09bc8-f051-4ffd-93d3-63c262064bb4",
"metadata": {},
"source": [
"# Show figure"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "11cf43c0-94fa-4e75-a85d-04a3f5c97729",
"metadata": {},
"outputs": [],
"source": [
"fig.show()"
]
},
{
"cell_type": "markdown",
"id": "ad58698e-1a21-466d-b640-78500cfcb229",
"metadata": {},
"source": [
"# Clear fig and user-created objects list"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ba9fffeb-45bd-4a0c-a941-e7c7e68f2e55",
"id": "5849b8b3-8765-4e37-868f-6be0d127bdee",
"metadata": {},
"outputs": [],
"source": [
"fig.clear()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8ea2206b-2522-40c2-beba-c3a377990219",
"metadata": {},
"outputs": [],
"source": [
"objects.clear()"
]
},
{
"cell_type": "markdown",
"id": "a7686046-65b6-4eb4-832a-7ca72c7f9bad",
"metadata": {},
"source": [
"# test gc"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e33bf32d-b13a-474b-92ca-1d1e1c7b820b",
"metadata": {},
"outputs": [],
"source": [
"test_references(objects)"
"test_references(weakrefs)"
]
},
{
"cell_type": "markdown",
"id": "4f927111-61c5-468e-8c90-b7b5338606ba",
"metadata": {},
"source": [
"# test for ImageWidget"
]
},
{
Expand Down Expand Up @@ -152,11 +225,11 @@
{
"cell_type": "code",
"execution_count": null,
"id": "38557b63-997f-433a-b744-e562e30be6ae",
"id": "7e855043-91c1-4f6c-bed3-b69cf4a87f84",
"metadata": {},
"outputs": [],
"source": [
"old_graphics = iw.managed_graphics\n",
"old_graphics = [weakref.proxy(g) for g in iw.managed_graphics]\n",
"\n",
"new_movies = [np.random.rand(100, 200, 200) for i in range(6)]\n",
"\n",
Expand All @@ -176,7 +249,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "712bb6ea-7244-4e03-8dfa-9419daa34915",
"id": "ad3d2a24-88b3-4071-a49c-49667d5a7813",
"metadata": {},
"outputs": [],
"source": []
Expand Down
30 changes: 10 additions & 20 deletions fastplotlib/graphics/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@


class Graphic:
_features = {}
_features: set[str] = {}

def __init_subclass__(cls, **kwargs):
# set the type of the graphic in lower case like "image", "line_collection", etc.
Expand Down Expand Up @@ -177,7 +177,7 @@ def block_events(self, value: bool):
def world_object(self) -> pygfx.WorldObject:
"""Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly."""
# We use weakref to simplify garbage collection
return weakref.proxy(WORLD_OBJECTS[self._fpl_address])
return weakref.proxy(WORLD_OBJECTS[hex(id(self))])

def _set_world_object(self, wo: pygfx.WorldObject):
WORLD_OBJECTS[self._fpl_address] = wo
Expand Down Expand Up @@ -348,24 +348,17 @@ def __repr__(self):
else:
return rval

def __eq__(self, other):
# This is necessary because we use Graphics as weakref proxies
if not isinstance(other, Graphic):
raise TypeError("`==` operator is only valid between two Graphics")

if self._fpl_address == other._fpl_address:
return True

return False

def _fpl_cleanup(self):
def _fpl_prepare_del(self):
"""
Cleans up the graphic in preparation for __del__(), such as removing event handlers from
plot renderer, feature event handlers, etc.

Optionally implemented in subclasses
"""
# remove event handlers
# signal that a deletion has been requested
self.deleted = True

# clear event handlers
self.clear_event_handlers()

# clear any attached event handlers and animation functions
Expand Down Expand Up @@ -394,13 +387,10 @@ def _fpl_cleanup(self):

self.world_object._event_handlers.clear()

for n in self._features:
fea = getattr(self, f"_{n}")
fea.clear_event_handlers()

def __del__(self):
self.deleted = True
del WORLD_OBJECTS[self._fpl_address]
# remove world object if created
# world object does not exist if an exception was raised during __init__ which is why this check exists
WORLD_OBJECTS.pop(hex(id(self)), None)

def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"):
"""Rotate the Graphic with respect to the world.
Expand Down
44 changes: 15 additions & 29 deletions fastplotlib/graphics/_collection_base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from contextlib import suppress
from typing import Any
import weakref

import numpy as np

from ._base import HexStr, Graphic

# Dict that holds all collection graphics in one python instance
COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict()
from ._base import Graphic


class CollectionProperties:
Expand Down Expand Up @@ -193,25 +190,17 @@ def __init__(self, name: str = None, metadata: Any = None, **kwargs):
super().__init__(name=name, metadata=metadata, **kwargs)

# list of mem locations of the graphics
self._graphics: list[str] = list()
self._graphics: list[Graphic] = list()

self._graphics_changed: bool = True
self._graphics_array: np.ndarray[Graphic] = None

self._iter = None

@property
def graphics(self) -> np.ndarray[Graphic]:
"""The Graphics within this collection. Always returns a proxy to the Graphics."""
if self._graphics_changed:
proxies = [
weakref.proxy(COLLECTION_GRAPHICS[addr]) for addr in self._graphics
]
self._graphics_array = np.array(proxies)
self._graphics_array.flags["WRITEABLE"] = False
self._graphics_changed = False
"""The Graphics within this collection."""

return self._graphics_array
return np.asarray(self._graphics)

def add_graphic(self, graphic: Graphic):
"""
Expand All @@ -231,10 +220,7 @@ def add_graphic(self, graphic: Graphic):
f"you are trying to add a {graphic.__class__.__name__}."
)

addr = graphic._fpl_address
COLLECTION_GRAPHICS[addr] = graphic

self._graphics.append(addr)
self._graphics.append(graphic)

self.world_object.add(graphic.world_object)

Expand All @@ -254,7 +240,7 @@ def remove_graphic(self, graphic: Graphic):

"""

self._graphics.remove(graphic._fpl_address)
self._graphics.remove(graphic)

self.world_object.remove(graphic.world_object)

Expand Down Expand Up @@ -313,7 +299,7 @@ def _fpl_add_plot_area_hook(self, plot_area):
for g in self:
g._fpl_add_plot_area_hook(plot_area)

def _fpl_cleanup(self):
def _fpl_prepare_del(self):
"""
Cleans up the graphic in preparation for __del__(), such as removing event handlers from
plot renderer, feature event handlers, etc.
Expand All @@ -324,20 +310,21 @@ def _fpl_cleanup(self):
self.world_object._event_handlers.clear()

for g in self:
g._fpl_cleanup()
g._fpl_prepare_del()

def __getitem__(self, key) -> CollectionIndexer:
if np.issubdtype(type(key), np.integer):
addr = self._graphics[key]
return weakref.proxy(COLLECTION_GRAPHICS[addr])
return self.graphics[key]

return self._indexer(selection=self.graphics[key], features=self._features)

def __del__(self):
# detach children
self.world_object.clear()

for addr in self._graphics:
del COLLECTION_GRAPHICS[addr]
for g in self.graphics:
g._fpl_prepare_del()
del g

super().__del__()

Expand All @@ -350,9 +337,8 @@ def __iter__(self):

def __next__(self) -> Graphic:
index = next(self._iter)
addr = self._graphics[index]

return weakref.proxy(COLLECTION_GRAPHICS[addr])
return self._graphics[index]

def __repr__(self):
rval = super().__repr__()
Expand Down
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy