Git

Vala: диагностика и чек-лист

Последняя страница раздела: чем искать утечки в программах на Vala и что проверить перед отправкой кода. Теория и примеры на страницах память и замыкания, GTK4 и сигналы, D-Bus и плагины, а реальные случаи в истории багов.

Чем искать утечки#

Valgrind#

Отключите слайсовый аллокатор GLib, чтобы valgrind видел настоящие malloc и free:

export G_SLICE=always-malloc export G_DEBUG=gc-friendly valgrind --leak-check=full \ --show-leak-kinds=definite,indirect \ --suppressions=/usr/share/glib-2.0/valgrind/glib.supp \ --num-callers=25 \ --log-file=valgrind.log \ ./build/my-gtk4-app

Как читать отчёт:

  • definitely lost это настоящая утечка, память недостижима;
  • still reachable формально достижима при выходе, это типично для намеренных синглтонов и не баг;
  • possibly lost указатель есть, но не «чистый», например указывает в середину блока.

Профиль кучи даёт Massif:

G_SLICE=always-malloc valgrind --tool=massif --massif-out-file=massif.out ./build/my-gtk4-app ms_print massif.out | less

Главное ограничение: циклы ссылок#

Valgrind, Massif и heaptrack плохо ловят циклы ссылок. Цикл это не «malloc без free»: объекты живы по счётчику, но логически недостижимы. Инструмент считает память достижимой и молчит.

Для циклов работает ручной метод:

  1. Проверить все .connect () и поля-делегаты на захват this.
  2. Добавить в подозрительный класс временный деструктор с сообщением:
~SuspiciousWidget () { message ("DESTROYED"); }
  1. Пересобрать, воспроизвести сценарий (открыть и закрыть окно) и посмотреть в журнал: сообщение появилось или нет.

Таким способом в панели нашли серию преждевременных уничтожений и утечек.

Тест со слабой ссылкой#

Самое надёжное доказательство утечки: слабая ссылка на объект, который должен был освободиться. Если после цикла уничтожения и обработки очереди событий ссылка не обнулилась, объект держит что-то ещё.

// Создание, слабая ссылка и проверка в одной функции // (GLib.WeakRef нельзя возвращать и копировать по значению) var obj = new MyWidget (); var wr = GLib.WeakRef (obj); obj = null; // отпускаем свою ссылку for (int i = 0; i < 20; i++) // даём сработать отложенным операциям GLib.MainContext.default ().iteration (false); if (wr.get () != null) warning ("Объект не уничтожен: возможен цикл ссылок");

В тестовом цикле повторяйте создание и удаление объекта десятки и сотни раз и ставьте слабые ссылки на несколько узлов дерева, а не на один корень. Между итерациями прогоняйте цикл событий (g_main_context_iteration), чтобы сработали отложенные операции вроде Idle.add-отвязки.

Ограничения теста: слабая ссылка видит только граф GObject. Кэши рендерера и аллокатора она не видит.

Счётчики объектов GObject#

В режиме полной диагностики панель показывает число живых объектов по классам. Для этого процесс должен быть запущен с переменной окружения GOBJECT_DEBUG=instance-count. GObject читает её при загрузке библиотеки, поэтому установить её изнутри main () бесполезно. Нужно либо задать её в окружении запуска, либо вызвать execv на самого себя сразу после установки.

Отладчик#

Счётчик ссылок объекта можно посмотреть в gdb:

(gdb) print ((GObject*) my_widget)->ref_count

Как отличить утечку от нормального роста#

Перед тем как заводить ошибку, ответьте на три вопроса:

  • Растёт ли без предела? Ограниченная история (например, до 50 уведомлений) и кэши заданного размера это поведение по замыслу, а не утечка.
  • Возвращается ли память после покоя? Крупные временные буферы оседают в куче glibc, и «anon» после них может не уменьшаться.
  • Подтверждено ли измерением? Правка по правилам не равна исправлению утечки. Считайте утечкой только то, что подтвердил тест или замер.

Как не ошибиться при тесте#

  • У headless-теста, который трогает D-Bus, обязательна своя dbus-daemon, иначе вы измерите чужой процесс. Сверяйте идентификатор процесса в ответе с тестовым.
  • Проверяйте текст ответа команды, а не только код выхода. Сервер может ответить ERR и всё равно завершиться с нулём.
  • Рендерер влияет на результат: тест на GSK_RENDERER=cairo не увидит утечки кэшей GL-рендерера, которые видны только на реальном GPU.
  • Плагины панели, собранные в домашний каталог, панель откажется загружать: плагины из $HOME считаются недоверенными. Для теста либо ставьте в /usr/lib64/niti/widgets/ через bootc usr-overlay, либо проверяйте виджет напрямую через привязку к его библиотеке.

Проверки в CI#

  • Компиляция с valac --fatal-warnings.
  • Линтер шаблонов Vala scripts/lint_vala_patterns.py в NitiVala. Он ищет опасные шаблоны: weak self в методах экземпляра, знаковый char для байтов UTF-8, запись пустых настроек после ошибки JSON, использование up () и down () для идентификаторов, чтение пути от внешнего кода без проверки, int64.parse () без проверки ошибки, изменение массива внутри foreach. Новые совпадения ломают сборку, известные хранятся в файле базовой линии.
  • Тесты с санитайзерами: MESON_SANITIZE=address,undefined bash build.sh, затем pytest.
  • Периодический прогон под valgrind: scripts/memcheck.sh.
  • Тесты через GI-привязки в pytest: быстрые, но видят только утечки по счётчику, а не циклы через сигналы.

Итоговый чек-лист#

Замыкания и сигналы#

  • [ ] Подписка на сигнал долгоживущего объекта не держит this без отписки.
  • [ ] Для лямбд, переданных в подписку или в таймер, используется weak-ссылка, а в методах экземпляра лучше статический метод с weak-параметрами.
  • [ ] В классах виджетов переопределён dispose (), в нём отключены все сигналы через disconnect (handler_id).
  • [ ] Timeout.add и Idle.add возвращают Source.REMOVE, когда объект мог исчезнуть, или снимаются через Source.remove (id) в dispose ().
  • [ ] Разрыв циклов, unbind () и отмена асинхронных задач делаются в dispose (), а не в деструкторе.

GVariant, массивы, делегаты#

  • [ ] Результаты get_child_value () и iter.next_value () хранятся в обычных (owned) переменных, не в unowned.
  • [ ] string[] в параметрах async-метода копируется до первого yield.
  • [ ] Не сохраняется срез массива, пока исходный массив может измениться.
  • [ ] Владение в описаниях сторонних библиотек (.vapi) сверено с реальным поведением API.

GTK4#

  • [ ] Контроллеры жестов это локальные переменные, а в замыкании используется unowned или weak-ссылка.
  • [ ] Поля [GtkChild] строго unowned.
  • [ ] В ListView подписки снимаются в unbind, лучше использовать Gtk.PropertyExpression.bind ().
  • [ ] Binding сохраняются и снимаются через unbind ().
  • [ ] При смене темы старый Gtk.CssProvider убирается перед добавлением нового.
  • [ ] Окна layer-shell создаются один раз и переиспользуются.
  • [ ] В анимациях над текстом нет scale ().

D-Bus#

  • [ ] Для signal_subscribe, watch_name, own_name и register_object сохранён идентификатор и есть парная отмена в dispose ().
  • [ ] Асинхронные вызовы идут с GLib.Cancellable, отмена в dispose (), после yield проверяется is_cancelled ().

Потоки#

  • [ ] Виджеты GTK трогаются только в главном потоке, фоновые потоки только считают.
  • [ ] Из фоновых потоков в Idle.add и Timeout.add используется weak-ссылка.
  • [ ] Последний unref из фонового потока передаётся в главный цикл через Idle.add.

Плагины#

  • [ ] Всё, что перечислено в чек-листе плагина: отписки, unparent (), таймеры, интерфейсы вместо лямбд.

Запись файлов и строки#

  • [ ] FileStream.write вызывается с одним аргументом, короткая запись проверяется.
  • [ ] В литералах с \x нет шестнадцатеричной цифры сразу после двух цифр.
  • [ ] При ошибке разбора JSON настройки пользователя не перезаписываются пустыми.