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»: объекты живы по счётчику, но логически недостижимы. Инструмент считает память достижимой и молчит.
Для циклов работает ручной метод:
- Проверить все
.connect ()и поля-делегаты на захватthis. - Добавить в подозрительный класс временный деструктор с сообщением:
~SuspiciousWidget () {
message ("DESTROYED");
}- Пересобрать, воспроизвести сценарий (открыть и закрыть окно) и посмотреть в журнал: сообщение появилось или нет.
Таким способом в панели нашли серию преждевременных уничтожений и утечек.
Тест со слабой ссылкой#
Самое надёжное доказательство утечки: слабая ссылка на объект, который должен был освободиться. Если после цикла уничтожения и обработки очереди событий ссылка не обнулилась, объект держит что-то ещё.
// Создание, слабая ссылка и проверка в одной функции
// (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 настройки пользователя не перезаписываются пустыми.