Skip to content

add References object to PlotArea, other cleanup, better garbage collection #467

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 8 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions examples/notebooks/test_gc.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "9dfba6cf-38af-4003-90b9-463c0cb1063f",
"metadata": {},
"outputs": [],
"source": [
"import fastplotlib as fpl\n",
"import numpy as np\n",
"import pytest"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7552eedc-3b9b-4682-8e3b-7d44e0e5510d",
"metadata": {},
"outputs": [],
"source": [
"def test_references(plot_objects):\n",
" 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]}\")"
]
},
{
"cell_type": "markdown",
"id": "948108e8-a4fa-4dc7-9953-a956428128cf",
"metadata": {},
"source": [
"# Add graphics and selectors, add feature event handlers, test gc occurs"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3d96bf14-b484-455e-bcd7-5b2fe7b45fb4",
"metadata": {},
"outputs": [],
"source": [
"xs = np.linspace(0, 20 * np.pi, 1_000)\n",
"ys = np.sin(xs)\n",
"zs = np.zeros(xs.size)\n",
"\n",
"points_data = np.column_stack([xs, ys, zs])\n",
"\n",
"line_collection_data = [points_data[:, 1].copy() for i in range(10)]\n",
"\n",
"img_data = np.random.rand(2_000, 2_000)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "284b83e1-8cfc-4105-b7c2-6214137dab31",
"metadata": {},
"outputs": [],
"source": [
"gp = fpl.GridPlot((2, 2))\n",
"\n",
"line = gp[0, 0].add_line(points_data, name=\"line\")\n",
"scatter = gp[0, 1].add_scatter(points_data.copy(), name=\"scatter\")\n",
"line_stack = gp[1, 0].add_line_stack(line_collection_data, name=\"line-stack\")\n",
"image = gp[1, 1].add_image(img_data, name=\"image\")\n",
"\n",
"linear_sel = line.add_linear_selector(name=\"line_linear_sel\")\n",
"linear_region_sel = line.add_linear_region_selector(name=\"line_region_sel\")\n",
"\n",
"linear_sel2 = line_stack.add_linear_selector(name=\"line-stack_linear_sel\")\n",
"linear_region_sel2 = line_stack.add_linear_region_selector(name=\"line-stack_region_sel\")\n",
"\n",
"linear_sel_img = image.add_linear_selector(name=\"image_linear_sel\")\n",
"linear_region_sel_img = image.add_linear_region_selector(name=\"image_linear_region_sel\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bb2083c1-f6b7-417c-86b8-9980819917db",
"metadata": {},
"outputs": [],
"source": [
"def feature_changed_handler(ev):\n",
" pass\n",
"\n",
"\n",
"objects = list()\n",
"for subplot in gp:\n",
" objects += subplot.objects\n",
"\n",
"\n",
"for g in objects:\n",
" for feature in g.feature_events:\n",
" if isinstance(g, fpl.LineCollection):\n",
" continue # skip collections for now\n",
" \n",
" f = getattr(g, feature)\n",
" f.add_event_handler(feature_changed_handler)\n",
"\n",
"gp.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ba9fffeb-45bd-4a0c-a941-e7c7e68f2e55",
"metadata": {},
"outputs": [],
"source": [
"gp.clear()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e33bf32d-b13a-474b-92ca-1d1e1c7b820b",
"metadata": {},
"outputs": [],
"source": [
"test_references(objects)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8078a7d2-9bc6-48a1-896c-7e169c5bbdcf",
"metadata": {},
"outputs": [],
"source": [
"movies = [np.random.rand(100, 100, 100) for i in range(6)]\n",
"\n",
"iw = fpl.ImageWidget(movies)\n",
"\n",
"# add some events onto all the image graphics\n",
"for g in iw.managed_graphics:\n",
" for f in g.feature_events:\n",
" fea = getattr(g, f)\n",
" fea.add_event_handler(feature_changed_handler)\n",
"\n",
"iw.show()"
]
},
{
"cell_type": "markdown",
"id": "189bcd7a-40a2-4e84-abcf-c334e50f5544",
"metadata": {},
"source": [
"# Test that setting new data with different dims clears old ImageGraphics"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "38557b63-997f-433a-b744-e562e30be6ae",
"metadata": {},
"outputs": [],
"source": [
"old_graphics = iw.managed_graphics\n",
"\n",
"new_movies = [np.random.rand(100, 200, 200) for i in range(6)]\n",
"\n",
"iw.set_data(new_movies)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "59e3c193-5672-4a66-bdca-12f1dd675d32",
"metadata": {},
"outputs": [],
"source": [
"test_references(old_graphics)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
78 changes: 60 additions & 18 deletions fastplotlib/graphics/_base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Literal
from typing import Any, Literal, TypeAlias
import weakref
from warnings import warn
from abc import ABC, abstractmethod
Expand All @@ -11,9 +11,12 @@

from ._features import GraphicFeature, PresentFeature, GraphicFeatureIndexable, Deleted


HexStr: TypeAlias = str

# dict that holds all world objects for a given python kernel/session
# Graphic objects only use proxies to WorldObjects
WORLD_OBJECTS: dict[str, WorldObject] = dict() #: {hex id str: WorldObject}
WORLD_OBJECTS: dict[HexStr, WorldObject] = dict() #: {hex id str: WorldObject}


PYGFX_EVENTS = [
Expand Down Expand Up @@ -80,7 +83,7 @@ def __init__(
self.present = PresentFeature(parent=self)

# store hex id str of Graphic instance mem location
self.loc: str = hex(id(self))
self._fpl_address: HexStr = hex(id(self))

self.deleted = Deleted(self, False)

Expand All @@ -93,19 +96,25 @@ def name(self) -> str | None:

@name.setter
def name(self, name: str):
if self.name == name:
return

if not isinstance(name, str):
raise TypeError("`Graphic` name must be of type <str>")

if self._plot_area is not None:
self._plot_area._check_graphic_name_exists(name)

self._name = name

@property
def world_object(self) -> 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[hex(id(self))])
return weakref.proxy(WORLD_OBJECTS[self._fpl_address])

def _set_world_object(self, wo: WorldObject):
WORLD_OBJECTS[hex(id(self))] = wo
WORLD_OBJECTS[self._fpl_address] = wo

@property
def position(self) -> np.ndarray:
Expand Down Expand Up @@ -166,6 +175,9 @@ def children(self) -> list[WorldObject]:
"""Return the children of the WorldObject."""
return self.world_object.children

def _fpl_add_plot_area_hook(self, plot_area):
self._plot_area = plot_area

def __setattr__(self, key, value):
if hasattr(self, key):
attr = getattr(self, key)
Expand All @@ -187,23 +199,52 @@ def __eq__(self, other):
if not isinstance(other, Graphic):
raise TypeError("`==` operator is only valid between two Graphics")

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

return False

def _cleanup(self):
def _fpl_cleanup(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
"""
pass
# clear any attached event handlers and animation functions
for attr in dir(self):
try:
method = getattr(self, attr)
except:
continue

if not callable(method):
continue

for ev_type in PYGFX_EVENTS:
try:
self._plot_area.renderer.remove_event_handler(method, ev_type)
except (KeyError, TypeError):
pass

try:
self._plot_area.remove_animation(method)
except KeyError:
pass

for child in self.world_object.children:
child._event_handlers.clear()

self.world_object._event_handlers.clear()

feature_names = getattr(self, "feature_events")
for n in feature_names:
fea = getattr(self, n)
fea.clear_event_handlers()

def __del__(self):
self.deleted = True
del WORLD_OBJECTS[self.loc]
del WORLD_OBJECTS[self._fpl_address]

def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"):
"""Rotate the Graphic with respect to the world.
Expand Down Expand Up @@ -372,7 +413,7 @@ def _event_handler(self, event):
else:
# get index of world object that made this event
for i, item in enumerate(self.graphics):
wo = WORLD_OBJECTS[item.loc]
wo = WORLD_OBJECTS[item._fpl_address]
# we only store hex id of worldobject, but worldobject `pick_info` is always the real object
# so if pygfx worldobject triggers an event by itself, such as `click`, etc., this will be
# the real world object in the pick_info and not the proxy
Expand Down Expand Up @@ -432,7 +473,8 @@ class PreviouslyModifiedData:
indices: Any


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


class GraphicCollection(Graphic):
Expand All @@ -450,7 +492,7 @@ 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[loc]) for loc in self._graphics
weakref.proxy(COLLECTION_GRAPHICS[addr]) for addr in self._graphics
]
self._graphics_array = np.array(proxies)
self._graphics_array.flags["WRITEABLE"] = False
Expand Down Expand Up @@ -479,10 +521,10 @@ def add_graphic(self, graphic: Graphic, reset_index: False):
f"you are trying to add a {graphic.__class__.__name__}."
)

loc = hex(id(graphic))
COLLECTION_GRAPHICS[loc] = graphic
addr = graphic._fpl_address
COLLECTION_GRAPHICS[addr] = graphic

self._graphics.append(loc)
self._graphics.append(addr)

if reset_index:
self._reset_index()
Expand All @@ -507,7 +549,7 @@ def remove_graphic(self, graphic: Graphic, reset_index: True):

"""

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

if reset_index:
self._reset_index()
Expand All @@ -525,8 +567,8 @@ def __getitem__(self, key):
def __del__(self):
self.world_object.clear()

for loc in self._graphics:
del COLLECTION_GRAPHICS[loc]
for addr in self._graphics:
del COLLECTION_GRAPHICS[addr]

super().__del__()

Expand Down
2 changes: 1 addition & 1 deletion fastplotlib/graphics/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs):

return bounds_init, limits, size, origin, axis, end_points

def _add_plot_area_hook(self, plot_area):
def _fpl_add_plot_area_hook(self, plot_area):
self._plot_area = plot_area

def set_feature(self, feature: str, new_data: Any, indices: Any = None):
Expand Down
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