LINUX.ORG.RU

[python] Утечки памяти

 


0

3

Писал тут игру на конкурс, и в самый последний день заметил, что при перезапуске уровня происходит жестокая утечка памяти: создается новый экземпляр уровня, а старый не удаляется, хотя внешних ссылок на него не остается. Поразмыслив и погуглив, пришел к выводу, что причина — циклические ссылки у объектов с деструкторами (gc их собрать не может ввиду неопределенности порядка вызова деструкторов). Был несколько огорчен, ибо немалая часть логики была завязана именно на циклических ссылках.

Вот ссылка на ревизию, в которой я еще ничего не пытался исправлять, а вот мои потуги в исправлении. Основная суть в level.py, player.py и game.py, в последнем из которых на 84 строчке пересоздается и утекает уровень. В общей сложности в игре чуть более тысячи строк, но интерес представляют всего где-то 600.

Какие циклы увидел я:

  • В game.py для space добавляются collision handler'ы — в level.space сохраняются ссылки на коллбэки, являющиеся методами level, т.е. фактически образуется цикл level → space → level. Вроде как решил переносом space в game.
  • player хранил ссылку на game (т.е. game → player → game), чтобы иметь возможность после смерти образовать капли крови и затем вернуться на чекпоинт. Решил передачей game в качестве параметра хэндлеру on_die.
  • Практически каждый класс в модуле level.py содержит ссылку на shape — объект типа pymunk.Shape. Но так как в collision handler передаются только ссылки на space и arbiter, из которых можно получить только физические объекты (тела и фигуры), мне нужно, чтобы фигура хранила еще некоторую дополнительную информацию (например, цвет у краски, next_level у выхода, id у триггера и прочее). Изначальное решение состояло в self.shape.sprite = self у объекта, что являлось явной циклической ссылкой. Решено частично, ибо при рисовании кляксы (level.py:365) мне нужно сразу обновить текстуру целевого объекта (т.е. нарисовать маску), а значит необходимо из фигуры иметь доступ к методу update_texture.

Из-за частичного решения третьего пункта и, вероятно, еще каких-то неучтенных деталей потребление памяти не уменьшилось.

Собственно, вопросы:

  • Все ли циклические ссылки я нашел?
  • Правильно ли я от них избавился, или это больше похоже на костыли, и ситуация требует пересмотра вообще всей архитектуры кода?
  • Нужно ли было вообще от них избавляться, или все можно было решить гораздо проще?
  • Как все-таки быть с обновлением текстуры, на которую был нарисован Blot?
  • Еще интересно, почему повышается потребление только VIRT памяти, а RES стабильно остается на уровне 40 мб, но это не мешает игре со временем уходить в своп.

Если у кого-то есть время и желание, большая просьба помочь разобраться с этими вещами.

★★★★★

фурри на моём лоре? НЕТ ПУТИ!

anonymous
()

Как бы запустить на маке?

$ python run.py 
Traceback (most recent call last):
  File «run.py», line 3, in <module>
    from itw.main import main
  File «/private/tmp/into-the-white/itw/main.py», line 6, in <module>
    from .window import window
  File «/private/tmp/into-the-white/itw/window.py», line 5, in <module>
    from pyglet.window.key import ESCAPE
  File «/Users/marko/.virtualenvs/main/lib/python2.7/site-packages/pyglet/window/__init__.py», line 1669, in <module>
    from pyglet.window.carbon import CarbonPlatform, CarbonWindow
  File «/Users/marko/.virtualenvs/main/lib/python2.7/site-packages/pyglet/window/carbon/__init__.py», line 69, in <module>
    framework='/System/Library/Frameworks/QuickTime.framework')
  File «/Users/marko/.virtualenvs/main/lib/python2.7/site-packages/pyglet/lib.py», line 90, in load_library
    return self.load_framework(kwargs['framework'])
  File «/Users/marko/.virtualenvs/main/lib/python2.7/site-packages/pyglet/lib.py», line 226, in load_framework
    lib = ctypes.cdll.LoadLibrary(realpath)
  File «/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/ctypes/__init__.py», line 431, in LoadLibrary
    return self._dlltype(name)
  File «/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/ctypes/__init__.py», line 353, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: dlopen(/System/Library/Frameworks/QuickTime.framework/QuickTime, 6): no suitable image found.  Did find:
	/System/Library/Frameworks/QuickTime.framework/QuickTime: mach-o, but wrong architecture
	/System/Library/Frameworks/QuickTime.framework/QuickTime: mach-o, but wrong architecture
mkevac
()
Ответ на: комментарий от mkevac

Если честно, понятия не имею. Попробуй, может, pyglet из hg:

hg clone https://pyglet.googlecode.com/hg/ pyglet
pevzi ★★★★★
() автор топика
Ответ на: комментарий от hippi90

слабые ссылки

Спасибо, почитаю.

pevzi ★★★★★
() автор топика
Ответ на: комментарий от pevzi

Я видел этот пост :) я очень долго думал что тебе написать. К сожалению слишком много текста. Вот если бы ты кратко описал проблему и минимальный кусок кода то можно было бы кастануть mv, beaverman и tailgunner. А так я посмотрел на код и патчи, ничего не понял и забил.

Я так понимаю у тебя классическая проблема высвобождения ресурсов. Я прихожу к выводу что __del__ это вообще зло и ресурсы должны высвобождаться какими-то событиями или сигналами в программе. А ещё стал использовать weakrefs где это возможно.

Опять-таки циклические ссылки могут свидетельствовать о проблеме в архитектуре приложения. Может можно перестроить иерархию классов?

А проблема была у меня несколько другая. Суть в том что gc работает не всегда, а только каждые 100 питонячих инструкций. Из-за этого объекты высвобождаются не сразу и на тривиальных примерах казалось что память течёт. А на самом деле она просто не сразу высвобождалась. Судя по тому что я не вижу у тебя в коде __del__ может это твой случай?

Ну и помни что программы (99%) умеют только расти вверх. Т.е. даже если ты высвобождаешь память она в ОС обратно не вернётся. Хотя, может и вернётся, посмотри http://stackoverflow.com/questions/2215259/will-malloc-implementations-return... и man malloc_trim

В общем, если ты опишешь проблему чуток проще и понятнее то я постараюсь помочь.

true_admin ★★★★★
()

Я не питонщик, но попробую дать пару советов. Во-первых GC может быть работает так, как я мою посуду - одна, две тарелки - пофиг, вот когда кран уже не видно - тогда пора что-то делать. Сделай уровень размером пару десятков мб (строк нагенери) и/или смени уровень тысячу раз и посмотри на расход памяти. Далее, по архитектуре. Размеется, у player просто не должно быть ссылок на level. На объекты уровня - тоже (шмотки, деколи или не знаю что). В частности убери space из параметров конструктора Player. Научи игрока существовать при закомментированном мире. Если что-то можно передавать в метод извне, а не хранить в объекте - так и делай. Хранение лишних данных создаёт лишние проблемы.

legolegs ★★★★★
()
Ответ на: комментарий от legolegs

тогда пора что-то делать

Ну, по дефолту он работает каждые 100 инструкций, но его можно заставить собрать мусор принудительно. Здесь же сразу было видно, что причина не в этом, ибо и после принудительной сборки мусор оставался.

В частности убери space из параметров конструктора Player
Научи игрока существовать при закомментированном мире

Хм, в конструкторе я создаю для игрока тело и фигуру, которые мне необходимо добавить в space, а сам space в игроке я не храню. То же самое происходит с объектами уровня. Создавать тела и фигуры в конструкторах, а потом заново пробегать по всем объектам с целью добавить их в space — как-то не комильфо, к тому же в ссылках разницы никакой нет.

Если что-то можно передавать в метод извне, а не хранить в объекте - так и делай. Хранение лишних данных создаёт лишние проблемы.

Это да, это я уже постиг. Везде где смог уже исправил.

pevzi ★★★★★
() автор топика
7 июня 2012 г.

Да, если кому-то интересно, проблему я все-таки решил. Вкратце: основная суть заключалась в том, что подлый pymunk при добавлении collision handler'ов создавал внутри себя какие-то объекты, имевшие ссылку на space (на графе эти петли хорошо видно). А так как у space есть деструктор, как раз эти циклические ссылки и создавали проблему. Решение очевидно — перед удалением space удалить все handler'ы:

        for key, value in self.space._handlers.items():
            a, b = key
            self.space.remove_collision_handler(a, b)
        self.space.set_default_collision_handler()

Собственно, ссылки на коммиты:

Хотя, конечно, стоит еще немного обдумать архитектуру, ибо не шибко нравится мне то, что получилось.

pevzi ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.