Workarounds for reference loops from Enum constructors

This commit is contained in:
Eric Froemling 2020-11-02 16:43:57 -06:00
parent 47ca132c33
commit e6b66074b9
12 changed files with 105 additions and 53 deletions

View File

@ -60,7 +60,8 @@ from ba._campaign import Campaign
from ba._gameutils import (GameTip, animate, animate_array, show_damage_count,
timestring, cameraflash)
from ba._general import (WeakCall, Call, existing, Existable,
verify_object_death, storagename, getclass)
verify_object_death, storagename, getclass,
enum_by_value)
from ba._keyboard import Keyboard
from ba._level import Level
from ba._lobby import Lobby, Chooser

View File

@ -8,6 +8,7 @@ import types
import weakref
import random
import inspect
from enum import Enum
from typing import TYPE_CHECKING, TypeVar, Protocol
from efro.terminal import Clr
@ -16,6 +17,7 @@ from ba._error import print_error, print_exception
from ba._enums import TimeType
if TYPE_CHECKING:
from types import FrameType
from typing import Any, Type, Optional
from efro.call import Call as Call # 'as Call' so we re-export.
from weakref import ReferenceType
@ -34,6 +36,7 @@ class Existable(Protocol):
ExistableType = TypeVar('ExistableType', bound=Existable)
T = TypeVar('T')
ET = TypeVar('ET', bound=Enum)
def existing(obj: Optional[ExistableType]) -> Optional[ExistableType]:
@ -306,18 +309,38 @@ def print_active_refs(obj: Any) -> None:
Useful for tracking down cyclical references and causes for zombie objects.
"""
from types import FrameType
# pylint: disable=too-many-nested-blocks
from types import FrameType, TracebackType
refs = list(gc.get_referrers(obj))
print(f'{Clr.YLW}Active referrers to {obj}:{Clr.RST}')
for i, ref in enumerate(refs):
print(f'{Clr.YLW}#{i+1}:{Clr.BLU} {ref}{Clr.RST}')
# For certain types of objects such as stack frames, show what is
# keeping *them* alive too.
if isinstance(ref, FrameType):
print(f'{Clr.YLW} Active referrers to #{i+1}:{Clr.RST}')
refs2 = list(gc.get_referrers(ref))
for j, ref2 in enumerate(refs2):
print(f'{Clr.YLW} #{j+1}:{Clr.BLU} {ref2}{Clr.RST}')
print(f'{Clr.YLW} #a{j+1}:{Clr.BLU} {ref2}{Clr.RST}')
# Can go further down the rabbit-hole if needed...
if bool(False):
if isinstance(ref2, TracebackType):
print(f'{Clr.YLW} '
f'Active referrers to #a{j+1}:{Clr.RST}')
refs3 = list(gc.get_referrers(ref2))
for k, ref3 in enumerate(refs3):
print(f'{Clr.YLW} '
f'#b{k+1}:{Clr.BLU} {ref3}{Clr.RST}')
if isinstance(ref3, BaseException):
print(f'{Clr.YLW} Active referrers to'
f' #b{k+1}:{Clr.RST}')
refs4 = list(gc.get_referrers(ref3))
for x, ref4 in enumerate(refs4):
print(f'{Clr.YLW} #c{x+1}:{Clr.BLU}'
f' {ref4}{Clr.RST}')
def _verify_object_death(wref: ReferenceType) -> None:
@ -376,3 +399,35 @@ def storagename(suffix: str = None) -> str:
if suffix is not None:
fullpath = f'{fullpath}_{suffix}'
return fullpath.replace('.', '_')
def _gut_exception(exc: Exception) -> None:
assert exc.__traceback__ is not None
frame: Optional[FrameType] = exc.__traceback__.tb_frame
while frame is not None:
frame.clear()
frame = frame.f_back
def enum_by_value(cls: Type[ET], value: Any) -> ET:
"""Create an enum from a value.
Category: General Utility Functions
This is basically the same as doing 'obj = EnumType(value)' except
that it works around an issue where a reference loop is created
if an exception is thrown due to an invalid value. Since we disable
the cyclic garbage collector for most of the time, such loops can lead
to our objects sticking around longer than we want. This workaround is
not perfect in that the destruction happens in the next cycle, but it is
better than never.
This issue has been submitted to Python as a bug so hopefully we can
remove this eventually if it gets fixed: https://bugs.python.org/issue42248
"""
try:
return cls(value)
except Exception as exc:
# Blow away all stack frames in the exception which will break the
# cycle and allow it to be destroyed.
_ba.pushcall(_Call(_gut_exception, exc))
raise

View File

@ -123,12 +123,13 @@ class UISubsystem:
# With our legacy main-menu system, the caller is responsible for
# clearing out the old main menu window when assigning the new.
# However there are corner cases where that doesn't happen and we get
# old windows stuck under the new main one. So let's guard against that
# However, we can't simply delete the existing main window when
# old windows stuck under the new main one. So let's guard against
# that. However, we can't simply delete the existing main window when
# a new one is assigned because the user may transition the old out
# *after* the assignment. Sigh. So as a happy medium let's check in
# *after* the assignment. Sigh. So, as a happy medium, let's check in
# on the old after a short bit of time and kill it if its still alive.
# That will be a bit ugly on screen but at least will un-break things.
# That will be a bit ugly on screen but at least should un-break
# things.
def _delay_kill() -> None:
import time
if existing:

View File

@ -133,7 +133,7 @@ class GatherWindow(ba.Window):
pos=(tab_buffer_h * 0.5,
self._height - 130 + tabs_top_extra),
size=(self._width - tab_buffer_h, 50),
on_select_call=self._set_tab)
on_select_call=ba.WeakCall(self._set_tab))
# Now instantiate handlers for these tabs.
tabtypes: Dict[GatherWindow.TabID, Type[GatherTab]] = {
@ -174,6 +174,7 @@ class GatherWindow(ba.Window):
texture=ba.gettexture('scrollWidget'),
model_transparent=ba.getmodel('softEdgeOutside'))
self._tab_container: Optional[ba.Widget] = None
self._restore_state()
def __del__(self) -> None:
@ -251,16 +252,11 @@ class GatherWindow(ba.Window):
current_tab = self.TabID.ABOUT
gather_tab_val = ba.app.config.get('Gather Tab')
try:
stored_tab = self.TabID(gather_tab_val)
stored_tab = ba.enum_by_value(self.TabID, gather_tab_val)
if stored_tab in self._tab_row.tabs:
current_tab = stored_tab
except ValueError:
# EWWWW; this exception causes a dependency loop that won't
# go away until the next cyclical collection, which can
# keep us alive. Perhaps should rethink our garbage
# collection strategy, but for now just explicitly running
# a cycle.
ba.pushcall(ba.garbage_collect)
pass
self._set_tab(current_tab)
if sel_name == 'Back':
sel = self._back_button
@ -268,15 +264,10 @@ class GatherWindow(ba.Window):
sel = self._tab_container
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
try:
sel_tab_id = self.TabID(sel_name.split(':')[-1])
sel_tab_id = ba.enum_by_value(self.TabID,
sel_name.split(':')[-1])
except ValueError:
sel_tab_id = self.TabID.ABOUT
# EWWWW; this exception causes a dependency loop that won't
# go away until the next cyclical collection, which can
# keep us alive. Perhaps should rethink our garbage
# collection strategy, but for now just explicitly running
# a cycle.
ba.pushcall(ba.garbage_collect)
sel = self._tab_row.tabs[sel_tab_id].button
else:
sel = self._tab_row.tabs[current_tab].button

View File

@ -184,12 +184,6 @@ class ManualGatherTab(GatherTab):
try:
port = int(cast(str, ba.textwidget(query=port_textwidget)))
except ValueError:
# EWWWW; this exception causes a dependency loop that won't
# go away until the next cyclical collection, which can
# keep us alive. Perhaps should rethink our garbage
# collection strategy, but for now just explicitly running
# a cycle.
ba.pushcall(ba.garbage_collect)
port = -1
if port > 65535 or port < 0:
ba.screenmessage(ba.Lstr(resource='internal.invalidPortErrorText'),

View File

@ -842,6 +842,10 @@ class PublicGatherTab(GatherTab):
key: val
for key, val in list(self._parties.items()) if val.claimed
}
# Make sure we update the list immediately in response to this.
self._server_list_dirty = True
self._update_server_list()
def _update(self) -> None:

View File

@ -71,7 +71,7 @@ class PartyWindow(ba.Window):
info = _ba.get_connection_to_host_info()
if info.get('name', '') != '':
title = info['name']
title = ba.Lstr(value=info['name'])
else:
title = ba.Lstr(resource=self._r + '.titleText')

View File

@ -1025,16 +1025,11 @@ class StoreBrowserWindow(ba.Window):
assert isinstance(sel_name, (str, type(None)))
try:
current_tab = self.TabID(ba.app.config.get('Store Tab'))
current_tab = ba.enum_by_value(self.TabID,
ba.app.config.get('Store Tab'))
except ValueError:
current_tab = self.TabID.CHARACTERS
# EWWWW; this exception causes a dependency loop that won't
# go away until the next cyclical collection, which can keep
# us alive. Perhaps should rethink our garbage collection
# strategy, but for now just explicitly running a cycle.
ba.pushcall(ba.garbage_collect)
if self._show_tab is not None:
current_tab = self._show_tab
if sel_name == 'GetTickets' and self._get_tickets_button:
@ -1045,7 +1040,8 @@ class StoreBrowserWindow(ba.Window):
sel = self._scrollwidget
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
try:
sel_tab_id = self.TabID(sel_name.split(':')[-1])
sel_tab_id = ba.enum_by_value(self.TabID,
sel_name.split(':')[-1])
except ValueError:
sel_tab_id = self.TabID.CHARACTERS
sel = self._tab_row.tabs[sel_tab_id].button

View File

@ -509,14 +509,9 @@ class WatchWindow(ba.Window):
{}).get('sel_name')
assert isinstance(sel_name, (str, type(None)))
try:
current_tab = self.TabID(ba.app.config.get('Watch Tab'))
current_tab = ba.enum_by_value(self.TabID,
ba.app.config.get('Watch Tab'))
except ValueError:
# EWWWW; this exception causes a dependency loop that won't
# go away until the next cyclical collection, which can
# keep us alive. Perhaps should rethink our garbage
# collection strategy, but for now just explicitly running
# a cycle.
ba.pushcall(ba.garbage_collect)
current_tab = self.TabID.MY_REPLAYS
self._set_tab(current_tab)
@ -526,14 +521,9 @@ class WatchWindow(ba.Window):
sel = self._tab_container
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
try:
sel_tab_id = self.TabID(sel_name.split(':')[-1])
sel_tab_id = ba.enum_by_value(self.TabID,
sel_name.split(':')[-1])
except ValueError:
# EWWWW; this exception causes a dependency loop that won't
# go away until the next cyclical collection, which can
# keep us alive. Perhaps should rethink our garbage
# collection strategy, but for now just explicitly running
# a cycle.
ba.pushcall(ba.garbage_collect)
sel_tab_id = self.TabID.MY_REPLAYS
sel = self._tab_row.tabs[sel_tab_id].button
else:

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2020-10-31 for Ballistica version 1.5.27 build 20236</em></h4>
<h4><em>last updated on 2020-11-02 for Ballistica version 1.5.27 build 20238</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr>
@ -86,6 +86,7 @@
<ul>
<li><a href="#function_ba_charstr">ba.charstr()</a></li>
<li><a href="#function_ba_do_once">ba.do_once()</a></li>
<li><a href="#function_ba_enum_by_value">ba.enum_by_value()</a></li>
<li><a href="#function_ba_garbage_collect">ba.garbage_collect()</a></li>
<li><a href="#function_ba_getclass">ba.getclass()</a></li>
<li><a href="#function_ba_is_browser_likely_available">ba.is_browser_likely_available()</a></li>
@ -6578,6 +6579,24 @@ the background and just looks pretty; it does not affect gameplay.
Note that the actual amount emitted may vary depending on graphics
settings, exiting element counts, or other factors.</p>
<hr>
<h2><strong><a name="function_ba_enum_by_value">ba.enum_by_value()</a></strong></h3>
<p><span>enum_by_value(cls: Type[ET], value: Any) -&gt; ET</span></p>
<p>Create an enum from a value.</p>
<p>Category: <a href="#function_category_General_Utility_Functions">General Utility Functions</a></p>
<p>This is basically the same as doing 'obj = EnumType(value)' except
that it works around an issue where a reference loop is created
if an exception is thrown due to an invalid value. Since we disable
the cyclic garbage collector for most of the time, such loops can lead
to our objects sticking around longer than we want. This workaround is
not perfect in that the destruction happens in the next cycle, but it is
better than never.
This issue has been submitted to Python as a bug so hopefully we can
remove this eventually if it gets fixed: https://bugs.python.org/issue42248</p>
<hr>
<h2><strong><a name="function_ba_existing">ba.existing()</a></strong></h3>
<p><span>existing(obj: Optional[ExistableType]) -&gt; Optional[ExistableType]</span></p>

View File

@ -21,7 +21,7 @@
namespace ballistica {
// These are set automatically via script; don't change here.
const int kAppBuildNumber = 20236;
const int kAppBuildNumber = 20238;
const char* kAppVersion = "1.5.27";
// Our standalone globals.

View File

@ -19,6 +19,7 @@
#include <sys/types.h>
#include <unistd.h>
#if BA_OSTYPE_ANDROID
// NOTE TO SELF: Apparently once we target API 24, ifaddrs.h is available.
#include "ballistica/platform/android/ifaddrs_android_ext.h"
#else
#include <ifaddrs.h>