Оригинал: pvs-studio.com
Вот интересная история о том, как наша команда искала ошибку в анализаторе PVS-Studio. Что ж, мы тоже делаем ошибки. Однако мы готовы засучить рукава и нырнуть в кроличью нору.
Небольшая предыстория
Наш коллега уже говорил о нашей техподдержке. Но всегда интересно почитать истории техподдержки. У нас они есть!
Если вам нужны сложные программы для программирования, вы можете сразу перейти к следующему разделу. Если вы хотите узнать о внутренней работе нашей службы поддержки, продолжайте читать :).
На данный момент у нас пять отделов разработки:
- отдел разработки анализаторов C и C++;
- отдел разработки C# анализатора;
- отдел инструментов и DevOps;
- отдел веб-разработки;
- отдел разработки CRM.
Первые два отдела, как следует из их названий, занимаются разработкой и поддержкой соответствующих статических анализаторов кода. Это включает в себя:
- разработка ядра анализатора: усовершенствования парсера и системы типов, улучшения анализа потоков данных и символьных вычислений и т. д. Кстати, недавно мы написали несколько статей о различных доработках: межмодульный анализ, борьба с устаревшим кодом в Анализатор C и C++, улучшение анализа потоков данных в анализаторе C#;
- написание новых диагностических правил и улучшение старых.
Третий отдел занимается разработкой и поддержкой всего дополнительного ПО для наших анализаторов:
- интеграция с популярными IDE — Visual Studio 2010–2022, IntelliJ IDEA, Rider, CLion;
- интеграция с платформой непрерывного контроля качества SonarQube;
- интеграция с игровыми движками Unreal Engine и Unity;
- утилита для преобразования отчета анализатора в различные форматы — SARIF, TeamCity, HTML, FullHtml и др.;
- утилита, уведомляющая команды разработчиков о подозрительных фрагментах кода.
Помимо разработки, все отделы также занимаются технической поддержкой. Каждый месяц мы выбираем одного или двух человек из каждого отдела для общения с пользователями по электронной почте. Обратите внимание: эти люди не сидят ни в каких колл-центрах и не занимаются первичной обработкой запросов. Для этого у нас есть еще один отдел с большим опытом. Им удается держать парней из отдела разработки подальше от большинства типичных вопросов пользователей, за исключением тех, которые технически сложны. Собственно, это тот момент, когда мы — разработчики — начинаем сотрудничать с техподдержкой для решения таких сложных задач. В большинстве случаев такие задачи потребуют исправления в коде. Мы считаем, что такой подход не только повышает качество и скорость техподдержки, но и демонстрирует разработчикам важность и актуальность разработанного ими функционала.
Теперь давайте подробнее рассмотрим поддержку отдела C++. Запросы на поддержку анализатора C и C++ можно разделить на следующие типы:
- Диагностическое правило выдает ложное срабатывание. Разработчику очень повезет, если пользователь отправит пример кода для воспроизведения проблемы. В большинстве случаев примеры, отправленные по электронной почте, сильно упрощены, и исправление диагностики иногда может стать тяжелым испытанием.
- Анализатор не выдает предупреждение на примере кода пользователя. Здесь возможны два исхода:
- анализатор специально не выдает предупреждения. Здесь вы можете подробнее узнать о причинах, по которым это происходит в некоторых случаях;
- анализатор специально не выдает предупреждения. Здесь вы можете подробнее узнать о причинах, по которым это происходит в некоторых случаях;
- пользователь прав. Получаем необходимые разъяснения на примере от пользователя, после чего решаем: либо дорабатываем существующую диагностику, либо пишем новую.
- Анализатор не понял некоторые конструкции языков C и C++. Грамматика этих языков позволяет писать сверхсложный код, и иногда анализатор не справляется. В таких ситуациях пользователи присылают нам ошибки V001. Для устранения таких проблем мы обычно запрашиваем примеры кода, которые можно воспроизвести, или промежуточные файлы для анализа (файлы *.i и *.cfg).
- Падение ядра анализатора C и C++. Никто не застрахован от ошибок, иногда случаются сбои. И это происходит и с нашим анализатором (V003). Наши пользователи очень помогают, отправляя нам трассировку стека, дамп памяти или промежуточные файлы для анализа.
- Один из многих вариантов использования продукта не работает. Проблемы такого рода чрезвычайно разнообразны, и описать их все в одном-двух предложениях невозможно.
История, упомянутая в заголовке статьи, началась как раз с обращения пользователя в службу поддержки. Клиент пожаловался на зависание инкрементального анализа, поэтому далее поговорим о последнем пункте списка выше.
Инкрементальный анализ, который не удался
История началась с того, что пользователь обратился в нашу службу поддержки со следующей проблемой:
- они запускают анализ в инкрементальном режиме или запускают проверку списка файлов;
- они распараллеливают анализ в N потоках;
- анализатор прекрасно работает до определенного времени в N потоках, а потом схлопывается в один поток. При этом в отчет начинает сыпаться куча ошибок V008, говорящих о невозможности препроцессинга файла.
Первое действие, которое нужно предпринять в этой ситуации, — просмотреть файл журнала. Просмотрев лог анализатора, присланный пользователем, мы обнаружили множество строк следующего вида:
Command "/usr/bin/c++ -DBOOST_ASIO_DYN_LINK ...." returned code 3.
Эта строка означает, что препроцессор перестал работать из-за тайм-аута. Мы запускаем препроцессор на скомпилированных файлах проекта, чтобы расширить макросы и сделать замены файлов, указанных в директивах #include. И только после этого запускаем анализ полученных файлов с некоторой дополнительной информацией (целевая платформа, пути к исключенным из анализа директориям и т.д.).
Многие разработчики C++ знакомы с трудностями компиляции проектов с включенными библиотеками Boost — время сборки значительно увеличивается. Это также влияет на предварительную обработку. Как видно из приведенной выше команды, пользователь использует Boost в проекте. Ранее мы также получали электронные письма с похожей проблемой: при высокой загрузке ЦП файлы не успевают препроцессироваться.
У нас уже давно была идея убрать 30-секундный тайм-аут препроцессинга. И тут у нас появился аналогичный случай. Решено — сняли таймаут. Отправляем пользователю бету и ждем ответа.
Мы уже собирались забыть об исправленной ошибке, когда пользователь сообщил о бета-версии:
- раньше анализ доходил до конца, но в отчете была куча V008;
- теперь анализ зависает на этапе разбора одних и тех же файлов (около 86% прогресса).
О каком парсинге файлов идет речь?
Что ж, проблема оказалась сложнее. Мы продолжали копать глубже.
Поскольку краш препроцессора пропал и теперь, видимо, зависало ядро анализатора C и C++, мы решили посмотреть сгенерированные конфигурационные файлы. И кажется, это именно то, что нам было нужно. Ничего необычного в настройках клиента не было, кроме одной маленькой детали:
exclude-path=*/generated/sip*
exclude-path=*/pacs/soapserver/generated/*
exclude-path=*/soap_engine/*
exclude-path=*/tech1utils/tests/googlemock/*
exclude-path=*/sdk-common/*
exclude-path=*/tech1grabbers/SDKs/*
# ....
# 200+ similar entries
# ....
exclude-path=/mnt/nvme/jenkins/workspace/..../lpr-ide.cpp
Параметр exclude-path позволяет отключить предупреждения о коде из сторонних библиотек и тестов. В стандартной ситуации пользователи либо указывают несколько путей к определенным каталогам, либо используют шаблон поиска. И количество записей редко превышает 30–40. В нашем случае было 200+ разных путей с исключенными файлами, включая шаблоны поиска. Мы подозревали, что наш алгоритм исключения файлов из анализа, написанный 10+ лет назад, просто не сможет быстро обработать такое количество записей в конфигурационном файле.
Почему он замедляется?
После оптимизации алгоритма на тестовом примере с 200+ исключенными путями в конфигурационном файле анализатор начал парсить и анализировать файлы в несколько раз быстрее. Это определенно был успех. Оставалось дело за малым — собрать бету, отдать ее пользователю и порадоваться своей маленькой победе.
Дворецкий сделал это!
Но праздновать победу было рано (закрыть тикет). Пользователь снова написал о том же зависании.
Что ж, быстрые исправления не помогли, нужно было еще глубже копаться в этом вопросе. Мы решили попросить пользователя запустить нашу утилиту на strace и отправить нам все сгенерированные журналы. Если кто не знает, утилита strace позволяет отслеживать все системные вызовы программы и многое другое. Кстати, мы используем его как один из вариантов запуска анализатора на проекте (компиляция трассировка).
Вот команда, которую пользователь использовал для создания журналов:
strace -y -v -s 4096 -ff -o strace-logs/log.txt -- pvs-studio-analyzer ....
Они оставили программу работать около 20 минут, прежде чем завершить процесс. Так как во время заморозки утилита strace продолжала записывать информацию в логи, размер логов получился внушительным — 22795 файлов общим весом 278 ГБ (!) без сжатия.
Сначала мы рассмотрели результаты strace. И сразу же мы увидели огромное количество вызовов nanosleep. Это означало, что дочерние процессы, сгенерированные утилитой pvs-studio-analyzer, по какой-то причине находились в состоянии ожидания. Прошерстили логи сверху вниз и нашли проблему (изображение кликабельно):
При клике по изображению на гифке будет видно, что номер дескриптора файла постепенно увеличивается после открытия файлов. После того, как это число приблизилось к значению 1024, при попытке выделить новый дескриптор возникла ошибка EMFILE, после чего анализ остановился. Такое поведение указывает на утечку файловых дескрипторов.
В ОС Linux при открытии файлу присваивается специальный номер — дескриптор. Затем дескриптор используется для работы с файлом: чтение, запись, просмотр атрибутов и т. д. Количество таких дескрипторов ограничено и определяется настройками системы.
Кстати, очень легко воспроизвести проблему. Для воспроизведения проблемы достаточно написать следующий CMakeLists.txt:
cmake_minimum_required(VERSION 3.5) project(many-files LANGUAGES C CXX)
set(SRC "")
foreach(i RANGE 10000) set(file "${CMAKE_CURRENT_BINARY_DIR}/src-${i}.c") file(TOUCH "${file}") set(SRC "${SRC};${file}") endforeach()
add_library(many-files STATIC ${SRC})
Далее формируем кеш в каталоге с CMakeLists.txt и запускаем утилиту pvs-studio-analyzer версии 7.18 и ранее:
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=On
pvs-studio-analyzer analyze -f ./build/compile_commands.json -j -i -o pvs.log
К сожалению, на момент написания этой статьи оригинальные логи канули в лету. Итак, на картинке выше лог, который мы воспроизвели сами.
Кто был виноват?
Мы исправили работу с ресурсами в программе и проблема исчезла. Подозреваем, что эта ошибка ранее ни у кого не возникала, так как Linux-версия анализатора чаще используется на билд-серверах в обычном режиме. Инкрементный анализ часто используется в сочетании с IDE, и на данный момент мы полностью поддерживаем только JetBrains CLion в Linux. Похоже, что до этого момента не было ни одного пользователя с необходимостью анализа проекта в инкрементальном режиме с большим количеством файлов.
Отдав бету клиенту в третий раз, мы наконец-то решили проблему с зависанием анализатора.
Заключение
К сожалению, не со всеми проблемами, поступающими в службу поддержки, легко справиться. Зачастую самые банальные баги лежат глубоко внутри и их сложно отладить.
Надеемся, что наша история была вам интересна. А если у вас возникнут проблемы с нашим продуктом, не стесняйтесь обращаться в нашу замечательную службу поддержки. Мы обещаем, что поможем вам.
Статьи по Теме
- Один день из жизни разработчика PVS-Studio.
- Хочешь сыграть детектива? Найдите ошибку в функции из Midnight Commander.
- Когда дворецкий становится жертвой.
- Программные ошибки, которых не существует.
- Как PVS-Studio оказалась внимательнее трех с половиной программистов.
- В очередной раз анализатор PVS-Studio оказался внимательнее человека.