Skip to content
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