Предлагаем вашему вниманию интересную историю о поиске бага внутри анализатора PVS-Studio. Да, мы тоже допускаем ошибки, но мы готовы засучить рукава и залезть в самую глубину "кроличьей норы".   Наш коллега уже рассказывал про нашу техническую поддержку. Но всегда интересно послушать какие-то истории, и они у нас есть. Если хочется программистской жести, то можете сразу переходить к следующему разделу. Если же хочется в целом познакомиться, как мы работаем, то продолжайте читать :). Также вы можете посмотреть юмористический доклад о поддержке С++ программистов. На текущий момент мы имеем пять отделов разработки: Первые два отдела, как подсказывают их названия, занимаются разработкой и поддержкой соответствующих статических анализаторов кода. Сюда входит: Третий отдел занимается разработкой и поддержкой всех связующих компонентов для наших анализаторов: Все отделы помимо разработки также занимаются и технической поддержкой. Для этого мы каждый месяц выделяем одного-двух человек из каждого отдела на общение с пользователями в почте. Обращаю внимание: ни в каких колл-центрах эти люди не сидят и не занимаются первичной обработкой заявок. Для этого у нас есть другой отдел с большим опытом работы. Им удаётся отгородить ребят от большинства типовых вопросов пользователей, кроме технически сложных. Собственно, для их решения мы, разработчики, и подключаемся. В большинстве случаев такие вопросы потребуют правок в коде. Мы считаем, что такой подход позволяет не только улучшить качество и быстроту работы поддержки, но и показать разработчикам значимость и востребованность разработанного ими функционала. Теперь же познакомимся поближе именно с поддержкой C++ отдела. Обращения в поддержку по C и C++ анализатору можно разделить на следующие виды: История, о которой говорится в заголовке статьи, началась как раз с письма пользователя в поддержку. Клиент жаловался на зависание инкрементального анализа, поэтому далее речь пойдёт именно о последнем варианте.  История началась с обращения пользователя в поддержку со следующей проблемой: Первое действие в этой ситуации, которое напрашивается само собой – это посмотреть лог. Изучив присланный пользователем лог анализатора, мы нашли множество записей вида: Сия запись означает, что препроцессор отвалился по таймауту. Мы запускаем препроцессор на компилируемых файлах проекта для того, чтобы раскрыть макросы и сделать подстановку файлов, указанных в директивах #include. И только после этого мы запускаем анализ на полученных файлах с некоторой дополнительной информацией (целевая платформа, пути до исключаемых директорий из анализа и т.д.). Многим C++ разработчикам знакома боль при компиляции проектов с подключенными библиотеками Boost – время сборки сильно повышается. Препроцессирование также страдает от этого. Как видно из вышеприведенной команды, пользователь использует в проекте Boost. Ранее нам также поступали письма с подобной проблемой: при высокой загрузке процессора файлы не успевают препроцессироваться. У нас уже достаточно давно витала в воздухе идея убрать это жёсткое ограничение с препроцессированием в 30 секунд. И снова похожий кейс. Решено – убираем таймаут. Можно высылать пользователю бету и ждать ответа.  Уже мы собирались забыть о пофикшенном баге, как пользователь отписывает нам о результатах с новой бетой: В силу свойств языков C и C++ инкрементальный анализ и режим проверки списка файлов требует анализа зависимостей компилируемых файлов. Например, если в режиме инкрементального анализа был модифицирован заголовочный файл, то нужно проверить любое включение этого файла в другие компилируемые файлы. В теории – это позволяет увидеть больше различных кейсов использования кода из заголовочного файла и улучшить качество анализа. Следовательно, если был модифицирован заголовочный файл, нужно найти все зависимые от него компилируемые файлы и поставить их на анализ. То же справедливо и для проверки списка файлов. Что ж, проблема оказалась более сложной, продолжаем копать.  Поскольку падение препроцессора исчезло и теперь, видимо, виснет ядро C и C++ анализатора, мы решили посмотреть генерируемые конфигурационные файлы. И, кажется, это как раз то, что нам нужно. В настройках клиента не было чего-то необычного, кроме одной маленькой детали: Настройка exclude-path позволяет подавлять предупреждения на код из third-party библиотек и тестов. В типовой ситуации пользователи указывают либо несколько путей до конкретных директорий, либо используют шаблон поиска. И количество записей редко превышает 30-40 штук. Здесь же было 200+ различных путей с исключенными файлами, включая шаблоны поиска. Мы заподозрили, что наш алгоритм исключения файлов из анализа, написанный 10+ лет назад, уже просто не мог быстро "переварить" такое количество записей в конфигурационном файле. Алгоритм исключения файлов из анализа работал так: Как можно легко заметить, алгоритм крайне не оптимизирован. Каждый путь из собранного списка сначала подвергается сканированию на наличие wildcard-символов – это полный проход по строке в худшем случае. Затем выбирается способ сравнения, и в худшем случае мы имеем уже 2 прохода по пути. И эти два прохода выполняются на все строки в списке. Первая оптимизация, которая сразу пришла в голову, – это заранее разделить исключаемые пути на шаблоны поиска (глобы) и обычные пути. Так в анализаторе родился новый класс — PathMatcher, который содержит 2 контейнера. Один контейнер для шаблонов и один для стандартных путей: Если с шаблонами поиска всё достаточно просто – их можно хранить в стандартном контейнере std::set для устранения дубликатов – то с путями есть нюанс. Да, здесь также можно воспользоваться ассоциативным контейнером вроде std::set, но в большинстве случаев переданные пользователем пути будут содержать какой-то общий префикс и различаться в самом конце: Всё намекает на структуру данных "префиксное дерево". Это позволяет оптимизировать как потребление памяти, так и поиск максимально длинного префикса. Поискав уже готовые реализации, мы остановились на Tessil/hat-trie. Для того чтобы различать файлы от директорий, мы применяем tsl::htrie_map, у которого ключом будет наш путь, а значением – тип файла. Теперь алгоритм работает примерно так: После оптимизации алгоритма на тестовом примере с 200+ исключёнными путями в конфигурационном файле анализатор в несколько раз быстрее стал приступать к парсингу и анализу файлов. Это определенно был успех. Дальше оставалось дело за малым – собрать бету, выдать пользователю и радоваться маленькой победе.  Но праздновать победу (закрывать тикет) было ещё рано. Пользователь опять пишет о том же самом зависании. Что ж, быстрые правки не помогли, приходится ещё сильнее погружаться в эту проблему. В этот раз мы решили попросить пользователя запустить нашу утилиту под strace и прислать все сформированные логи. Если кто не знает, утилита strace позволяет отследить все системные вызовы программы и многое другое. Кстати, мы же используем её для одного из вариантов внедрения анализатора в свой проект (трассировка вызовов компиляторов). Вот команда, которой пользователь формировал логи: Он оставил программу поработать примерно на 20 минут перед убийством процесса. Поскольку во время зависания утилита strace продолжала писать информацию в логи, то их размер получился внушительный – 22795 файлов с суммарным весом в 278 ГБ (!) без сжатия. Сначала посмотрели выхлоп strace. И сразу же увидели огромное количество вызовов nanosleep. Это означало, что дочерние процессы, порождаемые утилитой pvs-studio-analyzer, почему-то сидели в бесконечном ожидании. Мы прошерстили логи сверху вниз и таки нашли проблему:  При нажатии на картинку в анимации будет видно, что номер файлового дескриптора после открытия файлов постепенно возрастает. После того как этот номер приблизился к значению 1024, при попытке выделения нового дескриптора была выдана ошибка EMFILE, а затем анализ остановился. Такое поведение свидетельствует об утечке файловых дескрипторов. В ОС Linux при открытии файла ему присваивается специальный номер – дескриптор, который затем используется для работы с ним: чтения, записи, просмотра атрибутов и т.д. Количество таких дескрипторов ограничено и определяется настройками системы. Кстати, проблему воспроизвести очень легко. Для воспроизведения проблемы достаточно написать следующий CMakeLists.txt: Далее формируем кеш в директории с CMakeLists.txt и запускаем утилиту pvs-studio-analyzer версии ниже 7.18: К сожалению, на момент написания статьи оригинальные логи канули в лету. Так что на картинке выше лог с проблемой, который мы воспроизвели собственноручно. Как уже было ранее сказано, для корректной работы инкрементального режима и проверки списка файлов требуется анализ зависимостей компилируемых файлов. Для этого мы формируем особый файл depend_info.json, в котором отображаются зависимости компилируемых файлов от заголовочных. Некоторые исходные файлы могут быть многократно скомпилированы в разных проектах с разными флажками в пределах одного "решения". При формировании препроцессированного файла с постфиксом ".PVS-Studio.i" нам приходится отрезать расширение у имени исходного файла. Это сделано из-за того, что некоторые препроцессоры отказываются препроцессировать файл, если итоговый содержит в имени постфиксы вроде ".cpp", ".cxx" и др. Это может привести к коллизии, если, например, препроцессируются два файла – "source.cpp" и "source.cxx". Для устранения состояния гонки мы производим блокировку результирующего пути – создаётся и открывается особый файл ".pvslock". Если происходит коллизия, то к имени следующего препроцессированного файла добавится число 1, 2, 3 и т. д. После первого этапа парсинга мы уже имеем все необходимые препроцессированные файлы. Второй раз запускать препроцессор бессмысленно, и это может существенно сэкономить время последующего анализа. Поэтому после первого этапа мы переносим объект PreprocessedFile, который содержит все необходимые пути, а также объекты блокировки, в кеш. На втором этапе мы опрашиваем кеш, содержит ли он необходимый нам препроцессированный файл. Проблема была в том, что при перемещении объекта на первом этапе мы забывали снять блокировку. Это приводило к росту числа открытых временных файлов ".pvslock", и при превышении определенного числа файловых дескрипторов программа зависала. Правка была достаточно простой – при перемещении объекта в кеш теперь снимается блокировка и файл ".pvslock" закрывается и уничтожается. Мы исправили обработку ресурсов в программе, и проблема ушла. Подозреваем, что такая ошибка ранее ни у кого не возникала, т. к. Linux-версию анализатора больше используют на сборочных серверах в обычном режиме. Инкрементальный анализ чаще используют в связке с IDE, из которых на Linux мы полноценно поддерживаем только JetBrains CLion. Судя по всему, до того момента не находился пользователь с необходимостью анализировать проект в инкрементальном режиме с большим количеством файлов. Третий раз выкатив клиенту бету, мы, наконец, решили проблему с зависанием. К сожалению, не все проблемы, приходящие в поддержку, легко найти. Зачастую самые банальные ошибки лежат глубоко внутри и их сложно отлаживать. Надеемся, что наша история была интересна для вас. Ну а если у вас будут какие-либо проблемы с нашим продуктом, не стесняйтесь обращаться в нашу крутую поддержку, мы действительно поможем. Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: ссылка.