mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-28 18:15:45 +08:00
Workarounds for reference loops from Enum constructors
This commit is contained in:
parent
47ca132c33
commit
e6b66074b9
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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) -> 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]) -> Optional[ExistableType]</span></p>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user