Skip to content
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
66 changes: 66 additions & 0 deletions devDocs/unreachableObjects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Garbage collection errors
NVDA's `garbageHandler.py` monitors Python's cyclic garbage collector and reports
on objects that are unreachable.
Cyclic references are typically a symptom of bad design, and can cause major problems for certain objects.
For instance, cyclic references involving COM objects may cause a deadlock if the garbage collector happens to break the cycle and release the COM object in the wrong thread.

## How to know about a cyclic reference?
The log may contain errors like the following.
```
WARNING - garbageHandler.notifyObjectDeletion (10:45:23.171) - MainThread (21820):
Garbage collector has found one or more unreachable objects. See further warnings for specific objects.
...
WARNING - garbageHandler.notifyObjectDeletion (10:45:23.171) - MainThread (21820):
Deleting unreachable object <eventHandler._EventExecuter object at 0x1AC15350>
ERROR - garbageHandler._collectionCallback (10:45:23.172) - MainThread (21820):
Found at least 1 unreachable objects in run
```

## How to debug a cyclic reference?

Once you can reliably reproduce the log error, you can tell the garbage collector to save all unreachable objects.
After an unreachable object is detected the references to the unreachable object can be inspected via the python console.
Inspecting this should give you a fair idea of where the issue is occurring.

1. Open the NVDA Python console `NVDA+control+z`
1. Enable saving all objects:
``` python
import gc
gc.set_debug(gc.DEBUG_SAVEALL)
```
1. Reproduce the unreachable object error.
1. If garbage collection errors have not yet been logged, force a collect by calling:
``` python
gc.collect()
```

1. All unreachable objects will now be stored in `gc.garbage`.
It may be a very large list.
Some tricks for narrowing this list down:
- From the log, you can get the memory address (`id`) of the object.
Then use:
``` python
memoryAddress = 0xabcd123
obj = None
for o in gc.garbage:
if memoryAddress == id(o)
obj = o
```
- Listing the types collected, look for the type(s) matching the log message:
``` python
for index, o in enumerate(gc.garbage):
print(index, type(o))
```
1. Once you have a reference (`obj`) to the unreachable object, see what other objects refer to an object you can call.
You can do this by using `gc.get_referrers`.
The python console has a reference to `obj`, there may be a lot of output.
You can reduce this by looking at the types and following the most relevant.
``` python
for index, o in enumerate(gc.get_referrers(obj)):
print(index, type(o))
```
1. Continue following the references to build a picture of the cycle.

## Typical problems
Some examples of common issues:
- Exceptions caught and assigned to a local variable.
4 changes: 0 additions & 4 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ class CallCancelled(Exception):
"""Raised when a call is cancelled.
"""

# Apply several monkey patches to comtypes
# noinspection PyUnresolvedReferences
import comtypesMonkeyPatches

# Initialise comtypes.client.gen_dir and the comtypes.gen search path
# and Append our comInterfaces directory to the comtypes.gen search path.
import comtypes
Expand Down
17 changes: 2 additions & 15 deletions source/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,21 +560,8 @@ def initialize():
# otherwise the system default will be used
mainFrame.SetLayoutDirection(wxLang.LayoutDirection)
wx.GetApp().SetTopWindow(mainFrame)
# In wxPython >= 4.1,
# wx.CallAfter no longer executes callbacks while NVDA's main thread is within apopup menu or message box.
# To work around this,
# Monkeypatch wx.CallAfter to
# post a WM_NULL message to our top-level window after calling the original CallAfter,
# which causes wx's event loop to wake up enough to execute the callback.
old_wx_CallAfter = wx.CallAfter

def wx_CallAfter_wrapper(func, *args, **kwargs):
old_wx_CallAfter(func, *args, **kwargs)
# mainFrame may be None as NVDA could be terminating.
topHandle = mainFrame.Handle if mainFrame else None
if topHandle:
winUser.PostMessage(topHandle, winUser.WM_NULL, 0, 0)
wx.CallAfter = wx_CallAfter_wrapper
import monkeyPatches
monkeyPatches.applyWxMonkeyPatches(mainFrame, winUser, wx)


def terminate():
Expand Down
19 changes: 19 additions & 0 deletions source/monkeyPatches/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# A part of NonVisual Desktop Access (NVDA)
Comment thread
feerrenrut marked this conversation as resolved.
# Copyright (C) 2021 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

from . import wxMonkeyPatches


applyWxMonkeyPatches = wxMonkeyPatches.apply


def applyMonkeyPatches():
# Apply several monkey patches to comtypes
# F401 - imported but unused: Patches are applied during import
from . import comtypesMonkeyPatches # noqa: F401

# Apply patches to Enum, prevent cyclic references on ValueError during construction
from . import enumPatches
enumPatches.replace__new__()
60 changes: 60 additions & 0 deletions source/monkeyPatches/enumPatches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2021 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.


def replace__new__():
import enum
# prevent cyclic references on ValueError during construction
Comment thread
feerrenrut marked this conversation as resolved.
enum.Enum.__new__ = _replacement__new__


def _replacement__new__(cls, value):
""" Copied from python standard library enum.py class Enum.
Prevent cyclic references on ValueError during construction.
Local variable exc must be deleted, otherwise:
- ref to exc held by the frame
- ref to traceback held by exc
- ref to frame held by traceback
"""
# all enum instances are actually created during class construction
# without calling this method; this method is called by the metaclass'
# __call__ (i.e. Color(3) ), and by pickle
if type(value) is cls:
# For lookups like Color(Color.RED)
return value
# by-value search for a matching enum member
# see if it's in the reverse mapping (for hashable values)
try:
return cls._value2member_map_[value]
except KeyError:
# Not found, no need to do long O(n) search
pass
except TypeError:
# not there, now do long search -- O(n) behavior
for member in cls._member_map_.values():
if member._value_ == value:
return member
# still not found -- try _missing_ hook
try:
result = cls._missing_(value)
except Exception as e:
e.__context__ = ValueError("%r is not a valid %s" % (value, cls.__name__))
raise e

if isinstance(result, cls):
return result

with ValueError(
"%r is not a valid %s" % (value, cls.__name__)
) as ve_exc:
if result is None:
raise ve_exc

te_exc = TypeError(
'error in %s._missing_: returned %r instead of None or a valid member'
% (cls.__name__, result)
)
te_exc.__context__ = ve_exc
raise te_exc
23 changes: 23 additions & 0 deletions source/monkeyPatches/wxMonkeyPatches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2021 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.


def apply(mainFrame, winUser, wx):
# In wxPython >= 4.1,
# wx.CallAfter no longer executes callbacks while NVDA's main thread is within a popup menu or message box.
# To work around this,
# MonkeyPatch wx.CallAfter to
# post a WM_NULL message to our top-level window after calling the original CallAfter,
# which causes wx's event loop to wake up enough to execute the callback.
old_wx_CallAfter = wx.CallAfter

def wx_CallAfter_wrapper(func, *args, **kwargs):
old_wx_CallAfter(func, *args, **kwargs)
# mainFrame may be None as NVDA could be terminating.
topHandle = mainFrame.Handle if mainFrame else None
if topHandle:
winUser.PostMessage(topHandle, winUser.WM_NULL, 0, 0)

wx.CallAfter = wx_CallAfter_wrapper
3 changes: 3 additions & 0 deletions source/nvda.pyw
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import typing
import globalVars
import ctypes
from ctypes import wintypes
import monkeyPatches

monkeyPatches.applyMonkeyPatches()

#: logger to use before the true NVDA log is initialised.
# Ideally, all logging would be captured by the NVDA log, however this would introduce contention
Expand Down