Skip to content

Commit 675dfaa

Browse files
committed
use real refs to graphics
1 parent 89bf7cd commit 675dfaa

File tree

13 files changed

+179
-206
lines changed

13 files changed

+179
-206
lines changed

docs/source/api/layouts/subplot.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ Methods
5555
Subplot.clear
5656
Subplot.delete_graphic
5757
Subplot.get_rect
58-
Subplot.get_refcounts
5958
Subplot.insert_graphic
6059
Subplot.map_screen_to_world
6160
Subplot.remove_animation

examples/notebooks/test_gc.ipynb

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"metadata": {},
88
"outputs": [],
99
"source": [
10+
"import weakref\n",
1011
"import fastplotlib as fpl\n",
1112
"import numpy as np\n",
1213
"import pytest"
@@ -23,7 +24,7 @@
2324
" for i in range(len(plot_objects)):\n",
2425
" with pytest.raises(ReferenceError) as failure:\n",
2526
" plot_objects[i]\n",
26-
" pytest.fail(f\"GC failed for object: {objects[i]}\")"
27+
" pytest.fail(f\"GC failed for object: {plot_objects[i]} of type: {plot_objects[i].__class__.__name__}\")"
2728
]
2829
},
2930
{
@@ -49,7 +50,15 @@
4950
"\n",
5051
"line_collection_data = [points_data[:, 1].copy() for i in range(10)]\n",
5152
"\n",
52-
"img_data = np.random.rand(2_000, 2_000)"
53+
"img_data = np.random.rand(1_000, 1_000)"
54+
]
55+
},
56+
{
57+
"cell_type": "markdown",
58+
"id": "2a8a92e1-70bc-41b5-9ad8-b86dab6e74eb",
59+
"metadata": {},
60+
"source": [
61+
"# Make references to each graphic"
5362
]
5463
},
5564
{
@@ -76,50 +85,114 @@
7685
"linear_region_sel_img = image.add_linear_region_selector(name=\"image_linear_region_sel\")"
7786
]
7887
},
88+
{
89+
"cell_type": "markdown",
90+
"id": "d691c3c6-0d82-4aa8-90e9-165efffda369",
91+
"metadata": {},
92+
"source": [
93+
"# Add event handlers"
94+
]
95+
},
7996
{
8097
"cell_type": "code",
8198
"execution_count": null,
82-
"id": "bb2083c1-f6b7-417c-86b8-9980819917db",
99+
"id": "64198fd0-edd4-4ba1-8082-a65d57b83881",
83100
"metadata": {},
84101
"outputs": [],
85102
"source": [
86103
"def feature_changed_handler(ev):\n",
87-
" pass\n",
88-
"\n",
89-
"\n",
104+
" pass"
105+
]
106+
},
107+
{
108+
"cell_type": "code",
109+
"execution_count": null,
110+
"id": "4a86c37b-41ce-4b50-af43-ef61d36b7d81",
111+
"metadata": {},
112+
"outputs": [],
113+
"source": [
90114
"objects = list()\n",
115+
"weakrefs = list() # used to make sure the real objs are garbage collected\n",
91116
"for subplot in fig:\n",
92-
" objects += subplot.objects\n",
93-
"\n",
117+
" for obj in subplot.objects:\n",
118+
" objects.append(obj)\n",
119+
" weakrefs.append(weakref.proxy(obj))\n",
94120
"\n",
95121
"for g in objects:\n",
96122
" for feature in g._features:\n",
97-
" # if isinstance(g, fpl.LineCollection):?\n",
98-
" # continue # skip collections for now\n",
99-
" \n",
100-
" g.add_event_handler(feature_changed_handler, feature)\n",
101-
"\n",
123+
" g.add_event_handler(feature_changed_handler, feature)"
124+
]
125+
},
126+
{
127+
"cell_type": "markdown",
128+
"id": "ecd09bc8-f051-4ffd-93d3-63c262064bb4",
129+
"metadata": {},
130+
"source": [
131+
"# Show figure"
132+
]
133+
},
134+
{
135+
"cell_type": "code",
136+
"execution_count": null,
137+
"id": "11cf43c0-94fa-4e75-a85d-04a3f5c97729",
138+
"metadata": {},
139+
"outputs": [],
140+
"source": [
102141
"fig.show()"
103142
]
104143
},
144+
{
145+
"cell_type": "markdown",
146+
"id": "ad58698e-1a21-466d-b640-78500cfcb229",
147+
"metadata": {},
148+
"source": [
149+
"# Clear fig and user-created objects list"
150+
]
151+
},
105152
{
106153
"cell_type": "code",
107154
"execution_count": null,
108-
"id": "ba9fffeb-45bd-4a0c-a941-e7c7e68f2e55",
155+
"id": "5849b8b3-8765-4e37-868f-6be0d127bdee",
109156
"metadata": {},
110157
"outputs": [],
111158
"source": [
112159
"fig.clear()"
113160
]
114161
},
162+
{
163+
"cell_type": "code",
164+
"execution_count": null,
165+
"id": "8ea2206b-2522-40c2-beba-c3a377990219",
166+
"metadata": {},
167+
"outputs": [],
168+
"source": [
169+
"objects.clear()"
170+
]
171+
},
172+
{
173+
"cell_type": "markdown",
174+
"id": "a7686046-65b6-4eb4-832a-7ca72c7f9bad",
175+
"metadata": {},
176+
"source": [
177+
"# test gc"
178+
]
179+
},
115180
{
116181
"cell_type": "code",
117182
"execution_count": null,
118183
"id": "e33bf32d-b13a-474b-92ca-1d1e1c7b820b",
119184
"metadata": {},
120185
"outputs": [],
121186
"source": [
122-
"test_references(objects)"
187+
"test_references(weakrefs)"
188+
]
189+
},
190+
{
191+
"cell_type": "markdown",
192+
"id": "4f927111-61c5-468e-8c90-b7b5338606ba",
193+
"metadata": {},
194+
"source": [
195+
"# test for ImageWidget"
123196
]
124197
},
125198
{
@@ -152,11 +225,11 @@
152225
{
153226
"cell_type": "code",
154227
"execution_count": null,
155-
"id": "38557b63-997f-433a-b744-e562e30be6ae",
228+
"id": "7e855043-91c1-4f6c-bed3-b69cf4a87f84",
156229
"metadata": {},
157230
"outputs": [],
158231
"source": [
159-
"old_graphics = iw.managed_graphics\n",
232+
"old_graphics = [weakref.proxy(g) for g in iw.managed_graphics]\n",
160233
"\n",
161234
"new_movies = [np.random.rand(100, 200, 200) for i in range(6)]\n",
162235
"\n",
@@ -176,7 +249,7 @@
176249
{
177250
"cell_type": "code",
178251
"execution_count": null,
179-
"id": "712bb6ea-7244-4e03-8dfa-9419daa34915",
252+
"id": "ad3d2a24-88b3-4071-a49c-49667d5a7813",
180253
"metadata": {},
181254
"outputs": [],
182255
"source": []

fastplotlib/graphics/_base.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections import defaultdict
2+
from contextlib import suppress
23
from functools import partial
34
from typing import Any, Literal, TypeAlias
45
import weakref
@@ -177,7 +178,7 @@ def block_events(self, value: bool):
177178
def world_object(self) -> pygfx.WorldObject:
178179
"""Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly."""
179180
# We use weakref to simplify garbage collection
180-
return weakref.proxy(WORLD_OBJECTS[self._fpl_address])
181+
return weakref.proxy(WORLD_OBJECTS[hex(id(self))])
181182

182183
def _set_world_object(self, wo: pygfx.WorldObject):
183184
WORLD_OBJECTS[self._fpl_address] = wo
@@ -348,24 +349,17 @@ def __repr__(self):
348349
else:
349350
return rval
350351

351-
def __eq__(self, other):
352-
# This is necessary because we use Graphics as weakref proxies
353-
if not isinstance(other, Graphic):
354-
raise TypeError("`==` operator is only valid between two Graphics")
355-
356-
if self._fpl_address == other._fpl_address:
357-
return True
358-
359-
return False
360-
361-
def _fpl_cleanup(self):
352+
def _fpl_prepare_del(self):
362353
"""
363354
Cleans up the graphic in preparation for __del__(), such as removing event handlers from
364355
plot renderer, feature event handlers, etc.
365356
366357
Optionally implemented in subclasses
367358
"""
368-
# remove event handlers
359+
# signal that a deletion has been requested
360+
self.deleted = True
361+
362+
# clear event handlers
369363
self.clear_event_handlers()
370364

371365
# clear any attached event handlers and animation functions
@@ -394,13 +388,10 @@ def _fpl_cleanup(self):
394388

395389
self.world_object._event_handlers.clear()
396390

397-
for n in self._features:
398-
fea = getattr(self, f"_{n}")
399-
fea.clear_event_handlers()
400-
401391
def __del__(self):
402-
self.deleted = True
403-
del WORLD_OBJECTS[self._fpl_address]
392+
with suppress(KeyError):
393+
# remove world object if created
394+
del WORLD_OBJECTS[hex(id(self))]
404395

405396
def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"):
406397
"""Rotate the Graphic with respect to the world.

fastplotlib/graphics/_collection_base.py

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1+
from contextlib import suppress
12
from typing import Any
2-
import weakref
33

44
import numpy as np
55

6-
from ._base import HexStr, Graphic
7-
8-
# Dict that holds all collection graphics in one python instance
9-
COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict()
6+
from ._base import Graphic
107

118

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

195192
# list of mem locations of the graphics
196-
self._graphics: list[str] = list()
193+
self._graphics: list[Graphic] = list()
197194

198195
self._graphics_changed: bool = True
199-
self._graphics_array: np.ndarray[Graphic] = None
200196

201197
self._iter = None
202198

203199
@property
204200
def graphics(self) -> np.ndarray[Graphic]:
205-
"""The Graphics within this collection. Always returns a proxy to the Graphics."""
206-
if self._graphics_changed:
207-
proxies = [
208-
weakref.proxy(COLLECTION_GRAPHICS[addr]) for addr in self._graphics
209-
]
210-
self._graphics_array = np.array(proxies)
211-
self._graphics_array.flags["WRITEABLE"] = False
212-
self._graphics_changed = False
201+
"""The Graphics within this collection."""
213202

214-
return self._graphics_array
203+
return np.asarray(self._graphics)
215204

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

234-
addr = graphic._fpl_address
235-
COLLECTION_GRAPHICS[addr] = graphic
236-
237-
self._graphics.append(addr)
223+
self._graphics.append(graphic)
238224

239225
self.world_object.add(graphic.world_object)
240226

@@ -254,7 +240,7 @@ def remove_graphic(self, graphic: Graphic):
254240
255241
"""
256242

257-
self._graphics.remove(graphic._fpl_address)
243+
self._graphics.remove(graphic)
258244

259245
self.world_object.remove(graphic.world_object)
260246

@@ -313,7 +299,7 @@ def _fpl_add_plot_area_hook(self, plot_area):
313299
for g in self:
314300
g._fpl_add_plot_area_hook(plot_area)
315301

316-
def _fpl_cleanup(self):
302+
def _fpl_prepare_del(self):
317303
"""
318304
Cleans up the graphic in preparation for __del__(), such as removing event handlers from
319305
plot renderer, feature event handlers, etc.
@@ -324,20 +310,22 @@ def _fpl_cleanup(self):
324310
self.world_object._event_handlers.clear()
325311

326312
for g in self:
327-
g._fpl_cleanup()
313+
g._fpl_prepare_del()
328314

329315
def __getitem__(self, key) -> CollectionIndexer:
330316
if np.issubdtype(type(key), np.integer):
331-
addr = self._graphics[key]
332-
return weakref.proxy(COLLECTION_GRAPHICS[addr])
317+
return self.graphics[key]
333318

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

336321
def __del__(self):
337-
self.world_object.clear()
322+
# remove world object if it was created
323+
with suppress(KeyError):
324+
self.world_object.clear()
338325

339-
for addr in self._graphics:
340-
del COLLECTION_GRAPHICS[addr]
326+
for g in self.graphics:
327+
g._fpl_prepare_del()
328+
del g
341329

342330
super().__del__()
343331

@@ -350,9 +338,8 @@ def __iter__(self):
350338

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

355-
return weakref.proxy(COLLECTION_GRAPHICS[addr])
342+
return self._graphics[index]
356343

357344
def __repr__(self):
358345
rval = super().__repr__()

0 commit comments

Comments
 (0)