Оригинал: pvs-studio.com

Вот интересная история о том, как наша команда искала ошибку в анализаторе PVS-Studio. Что ж, мы тоже делаем ошибки. Однако мы готовы засучить рукава и нырнуть в кроличью нору.

Небольшая предыстория

Наш коллега уже говорил о нашей техподдержке. Но всегда интересно почитать истории техподдержки. У нас они есть!

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

На данный момент у нас пять отделов разработки:

  • отдел разработки анализаторов C и C++;
  • отдел разработки C# анализатора;
  • отдел инструментов и DevOps;
  • отдел веб-разработки;
  • отдел разработки CRM.

Первые два отдела, как следует из их названий, занимаются разработкой и поддержкой соответствующих статических анализаторов кода. Это включает в себя:

Третий отдел занимается разработкой и поддержкой всего дополнительного ПО для наших анализаторов:

  • интеграция с популярными IDE — Visual Studio 2010–2022, IntelliJ IDEA, Rider, CLion;
  • интеграция с платформой непрерывного контроля качества SonarQube;
  • интеграция с игровыми движками Unreal Engine и Unity;
  • утилита для преобразования отчета анализатора в различные форматы — SARIF, TeamCity, HTML, FullHtml и др.;
  • утилита, уведомляющая команды разработчиков о подозрительных фрагментах кода.

Помимо разработки, все отделы также занимаются технической поддержкой. Каждый месяц мы выбираем одного или двух человек из каждого отдела для общения с пользователями по электронной почте. Обратите внимание: эти люди не сидят ни в каких колл-центрах и не занимаются первичной обработкой запросов. Для этого у нас есть еще один отдел с большим опытом. Им удается держать парней из отдела разработки подальше от большинства типичных вопросов пользователей, за исключением тех, которые технически сложны. Собственно, это тот момент, когда мы — разработчики — начинаем сотрудничать с техподдержкой для решения таких сложных задач. В большинстве случаев такие задачи потребуют исправления в коде. Мы считаем, что такой подход не только повышает качество и скорость техподдержки, но и демонстрирует разработчикам важность и актуальность разработанного ими функционала.

Теперь давайте подробнее рассмотрим поддержку отдела C++. Запросы на поддержку анализатора C и C++ можно разделить на следующие типы:

  1. Диагностическое правило выдает ложное срабатывание. Разработчику очень повезет, если пользователь отправит пример кода для воспроизведения проблемы. В большинстве случаев примеры, отправленные по электронной почте, сильно упрощены, и исправление диагностики иногда может стать тяжелым испытанием.
  2. Анализатор не выдает предупреждение на примере кода пользователя. Здесь возможны два исхода:
  • анализатор специально не выдает предупреждения. Здесь вы можете подробнее узнать о причинах, по которым это происходит в некоторых случаях;
  1. анализатор специально не выдает предупреждения. Здесь вы можете подробнее узнать о причинах, по которым это происходит в некоторых случаях;
  • пользователь прав. Получаем необходимые разъяснения на примере от пользователя, после чего решаем: либо дорабатываем существующую диагностику, либо пишем новую.
  • Анализатор не понял некоторые конструкции языков 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. Похоже, что до этого момента не было ни одного пользователя с необходимостью анализа проекта в инкрементальном режиме с большим количеством файлов.

Отдав бету клиенту в третий раз, мы наконец-то решили проблему с зависанием анализатора.

Заключение

К сожалению, не со всеми проблемами, поступающими в службу поддержки, легко справиться. Зачастую самые банальные баги лежат глубоко внутри и их сложно отладить.

Надеемся, что наша история была вам интересна. А если у вас возникнут проблемы с нашим продуктом, не стесняйтесь обращаться в нашу замечательную службу поддержки. Мы обещаем, что поможем вам.

Статьи по Теме