﻿# Как мы баг в PVS\-Studio искали или 278 Гигабайтов логов

Предлагаем вашему вниманию интересную историю о поиске бага внутри анализатора PVS\-Studio\. Да, мы тоже допускаем ошибки, но мы готовы засучить рукава и залезть в самую глубину "кроличьей норы"\. 

![1005_StoriesFromSupport_ru/image1.png](https://import.viva64.com/docx/blog/1005_StoriesFromSupport_ru/image1.png)

## Небольшое предисловие

Наш коллега уже рассказывал про нашу [техническую поддержку](https://pvs-studio.ru/ru/blog/posts/0861/)\. Но всегда интересно послушать какие\-то истории, и они у нас есть\.

Если хочется программисткой жести, то можете сразу переходить к следующему разделу\. Если же хочется в целом познакомиться, как мы работаем, то продолжайте читать :\)\. Также вы можете посмотреть юмористический [доклад](https://pvs-studio.ru/ru/blog/video/10058/) о поддержке С\+\+ программистов\.

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

* отдел разработки C и C\+\+ анализатора;
* отдел разработки C\# анализатора;
* отдел Tools & DevOps;
* отдел web\-разработки;
* отдел разработки CRM\-системы\.

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

* разработка ядра анализатора: улучшение парсера и системы типов, улучшение анализа потока данных и символьных вычислений и др\. Кстати, недавно мы написали несколько статей о различных доработках: [межмодульный анализ](https://pvs-studio.ru/ru/blog/posts/cpp/0965/), [борьба с легаси в C и C\+\+ анализаторе](https://pvs-studio.ru/ru/blog/posts/cpp/0992/), [улучшение анализа потока данных в C\# анализаторе](https://pvs-studio.ru/ru/blog/posts/csharp/0976/);
* написание новых диагностических правил и улучшение старых\.

Третий отдел занимается разработкой и поддержкой всех связующих компонентов для наших анализаторов:

* интеграция с популярными IDE – Visual Studio 2010\-2022, IntelliJ IDEA, Rider, CLion;
* интеграция с платформой непрерывного контроля качества SonarQube;
* интеграция с игровыми движками Unreal Engine и Unity;
* утилита для конвертации отчёта анализатора в различные форматы – SARIF, TeamCity, HTML, [FullHTML](https://pvs-studio.ru/ru/blog/posts/0539/) и др\.;
* утилита для оповещения команд разработчиков о найденных подозрительных местах в коде\.

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

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

1. Диагностическое правило выдаёт ложноположительное срабатывание\. Разработчику сильно повезёт, если пользователь присылает пример для воспроизведения\. В большинстве случаев присланные по переписке примеры максимально упрощаются, и поправить диагностику иногда становится испытанием\.
1. Анализатор не выдаёт срабатывание на пользовательском примере\. Здесь возможны два исхода:
    * анализатор молчит специально\. [Здесь](https://pvs-studio.ru/ru/blog/posts/cpp/0488/) вы можете подробнее ознакомиться с причинами, почему он это делает в некоторых ситуациях;
    * пользователь прав\. Мы получаем от него необходимые уточнения по примеру, а дальше решаем: либо дорабатываем существующую диагностику, либо пишем новую\.
1. Анализатор не разобрал какую\-либо конструкцию языка C и C\+\+\. Грамматики этих языков позволяют писать очень запутанный код, и порой анализатор не справляется\. В таких ситуациях пользователи присылают нам ошибки [V001](https://pvs-studio.ru/ru/docs/warnings/v001/)\. Чтобы исправлять такие проблемы, обычно мы запрашиваем минимально воспроизводимые примеры или промежуточные файлы для анализа \(\*\.i и \*\.cfg файлы\)\.
1. Падение ядра C и C\+\+ анализатора\. От ошибок не застрахован никто, падения иногда происходят\. С нашим анализатором тоже \([V003](https://pvs-studio.ru/ru/docs/warnings/v003/)\)\. Здесь очень помогают пользователи, присылая стектрейсы, дампы памяти или промежуточные файлы для анализа\.
1. Не работает один из многих сценариев использования продукта\. Проблемы подобного рода обладают широчайшим разнообразием, и описать их всех в паре предложений не удастся\.

История, о которой говорится в заголовке статьи, началась как раз с письма пользователя в поддержку\. Клиент жаловался на зависание инкрементального анализа, поэтому далее речь пойдёт именно о последнем варианте\.

## Инкрементальный анализ, который не смог

![1005_StoriesFromSupport_ru/image2.png](https://import.viva64.com/docx/blog/1005_StoriesFromSupport_ru/image2.png)

История началась с обращения пользователя в поддержку со следующей проблемой:

* запускаем анализ в инкрементальном режиме или проверки списка файлов;
* параллелим анализ в N потоков;
* анализатор прекрасно работает до определённого времени в N потоков, а затем "схлопывается" до одного\. При этом в отчёт начинает сыпаться куча ошибок [V008](https://pvs-studio.ru/ru/docs/warnings/v008/), которые сообщают о невозможности препроцессировать файл\.

Первое действие в этой ситуации, которое напрашивается само собой – это посмотреть лог\. Изучив присланный пользователем лог анализатора, мы нашли множество записей вида:

```cpp
Command "/usr/bin/c++ -DBOOST_ASIO_DYN_LINK ...." returned code 3.
```

Сия запись означает, что препроцессор отвалился по таймауту\. Мы запускаем препроцессор на компилируемых файлах проекта для того, чтобы раскрыть макросы и сделать подстановку файлов, указанных в директивах _\#include_\. И только после этого мы запускаем анализ на полученных файлах с некоторой дополнительной информацией \(целевая платформа, пути до исключаемых директорий из анализа и т\.д\.\)\.

Многим C\+\+ разработчикам знакома боль при компиляции проектов с подключенными библиотеками Boost – время сборки сильно повышается\. Препроцессирование также страдает от этого\. Как видно из вышеприведенной команды, пользователь использует в проекте Boost\. Ранее нам также поступали письма с подобной проблемой: при высокой загрузке процессора файлы не успевают препроцессироваться\.

У нас уже достаточно давно витала в воздухе идея убрать это жёсткое ограничение с препроцессированием в 30 секунд\. И снова похожий кейс\. Решено – убираем таймаут\. Можно высылать пользователю бету и ждать ответа\. 

Уже мы собирались забыть о пофикшенном баге, как пользователь отписывает нам о результатах с новой бетой:

* ранее анализ доходил до конца, но была куча V008 в отчёте;
* теперь анализ зависает на этапе парсинга тех же самых файлов \(примерно на 86 % прогресса\)\.

<details>
   <summary>Что же это за парсинг файлов такой?</summary>

В силу свойств языков C и C\+\+ инкрементальный анализ и режим проверки списка файлов требует анализа зависимостей компилируемых файлов\. Например, если в режиме инкрементального анализа был модифицирован заголовочный файл, то нужно проверить любое включение этого файла в другие компилируемые файлы\. В теории – это позволяет увидеть больше различных кейсов использования кода из заголовочного файла и улучшить качество анализа\.

Следовательно, если был модифицирован заголовочный файл, нужно найти все зависимые от него компилируемые файлы и поставить их на анализ\. То же справедливо и для проверки списка файлов\.


</details>


Что ж, проблема оказалась более сложной, продолжаем копать\.

![1005_StoriesFromSupport_ru/image3.png](https://import.viva64.com/docx/blog/1005_StoriesFromSupport_ru/image3.png)

Поскольку падение препроцессора исчезло и теперь, видимо, виснет ядро C и C\+\+ анализатора, мы решили посмотреть генерируемые конфигурационные файлы\. И, кажется, это как раз то, что нам нужно\. В настройках клиента не было чего\-то необычного, кроме одной маленькой детали:

```cpp
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_ позволяет подавлять предупреждения на код из third\-party библиотек и тестов\.  В типовой ситуации пользователи указывают либо несколько путей до конкретных директорий, либо используют [шаблон поиска](https://ru.wikipedia.org/wiki/%D0%A8%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0)\. И количество записей редко превышает 30\-40 штук\. Здесь же было 200\+ различных путей с исключенными файлами, включая шаблоны поиска\. Мы заподозрили, что наш алгоритм исключения файлов из анализа, написанный 10\+ лет назад, уже просто не мог быстро "переварить" такое количество записей в конфигурационном файле\.

<details>
   <summary>Почему оно тормозит?</summary>

Алгоритм исключения файлов из анализа работал так:

1. Собираем платформо\-специфичные пути до директорий с системными библиотеками\. В C и C\+\+ анализаторе уже "вшиты" некоторые стандартные пути, например:
    * "?:\\\\program files \(x86\)\\\\microsoft visual studio \*\\\\vc\\\\\*"
    * "/usr/include/"
    * "/usr/local/Cellar"
    * и другие пути, примерно до 30 штук\.
1. Объединяем их с путями, заданными пользователем\.
1. Сопоставляем входной путь с каждым из собранного списка:
    * если исключаемый путь содержит символы "?" или "\*", то используем платформо\-специфичную функцию для поиска по шаблону\. На Windows – это [PathMatchSpec](https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-pathmatchspecw), на \*nix\-подобных ОС – [fnmatch](https://man7.org/linux/man-pages/man3/fnmatch.3.html);
    * иначе проверяем, начинается ли входной путь с пути из собранного списка\. При сравнении строк используется платформо\-специфичная функция сравнения\. Как мы помним, на Windows сравнение путей происходит без учёта регистра, на \*nix\-подобных ОС – преимущественно с учётом регистра\.

Как можно легко заметить, алгоритм крайне не оптимизирован\. **Каждый** путь из собранного списка сначала подвергается сканированию на наличие wildcard\-символов – это полный проход по строке в худшем случае\. Затем выбирается способ сравнения, и в худшем случае мы имеем уже 2 прохода по пути\. И эти два прохода выполняются на все строки в списке\.

Первая оптимизация, которая сразу пришла в голову, – это заранее разделить исключаемые пути на шаблоны поиска \(глобы\) и обычные пути\. Так в анализаторе родился новый класс —  _PathMatcher_, который содержит 2 контейнера\. Один контейнер для шаблонов и один для стандартных путей:

```cpp
class PathMatcher
{
// ....
private:
  using GlobsCollection = std::set<std::string, std::less<>>;
  using PathsCollection = ???;

  GlobsCollection m_globs; // шаблоны
  PathsCollection m_paths; // обычные пути
};
```

Если с шаблонами поиска всё достаточно просто – их можно хранить в стандартном контейнере _std::set_ для устранения дубликатов – то с путями есть нюанс\. Да, здесь также можно воспользоваться ассоциативным контейнером вроде _std::set_, но в большинстве случаев переданные пользователем пути будут содержать какой\-то общий префикс и различаться в самом конце:

```cpp
/home/user/folderToExclude/fileToExclude.cpp
/home/user/folderToExclude/
|______общий префикс______|
```

Всё намекает на структуру данных "[префиксное дерево](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE)"\. Это позволяет оптимизировать как потребление памяти, так и поиск максимально длинного префикса\. Поискав уже готовые реализации, мы остановились на [Tessil/hat\-trie](https://github.com/Tessil/hat-trie)\. Для того чтобы различать файлы от директорий, мы применяем _tsl::htrie\_map_, у которого ключом будет наш путь, а значением – тип файла\.

Теперь алгоритм работает примерно так:

1. При переборе конфигурационного файла мы определяем, в какой контейнер внутри класса PathMatcher класть исключенный путь:
    * если был найден wildcard\-символ, то кладём в контейнер для шаблонов;
    * иначе кладём в префиксное дерево\.
1. Сопоставляем входной путь с путями внутри класса PathMatcher так:
    * ищем общий префикс с путями в префиксном дереве\. Если он находится, то файл исключается из анализа;
    * иначе мы перебираем все шаблоны и вызываем платформо\-специфичные функции для сравнения с входным файлом\.


</details>


После оптимизации алгоритма на тестовом примере с 200\+ исключёнными путями в конфигурационном файле анализатор в несколько раз быстрее стал приступать к парсингу и анализу файлов\. Это определенно был успех\. Дальше оставалось дело за малым – собрать бету, выдать пользователю и радоваться маленькой победе\.

## Убийца – дворецкий\!

![1005_StoriesFromSupport_ru/image4.png](https://import.viva64.com/docx/blog/1005_StoriesFromSupport_ru/image4.png)

Но праздновать победу \(закрывать тикет\) было ещё рано\. Пользователь опять пишет о том же самом зависании\.

Что ж, быстрые правки не помогли, приходится ещё сильнее погружаться в эту проблему\. В этот раз мы решили попросить пользователя запустить нашу утилиту под [_strace_](https://man7.org/linux/man-pages/man1/strace.1.html) и прислать все сформированные логи\. Если кто не знает, утилита _strace_ позволяет отследить все системные вызовы программы и многое другое\. Кстати, мы же используем её для одного из вариантов внедрения анализатора в свой проект \([трассировка](https://pvs-studio.ru/ru/docs/manual/0036/) вызовов компиляторов\)\.

Вот команда, которой пользователь формировал логи:

```cpp
strace -y -v -s 4096 -ff -o strace-logs/log.txt -- pvs-studio-analyzer ....
```

Он оставил программу поработать примерно на 20 минут перед убийством процесса\. Поскольку во время зависания утилита _strace_ продолжала писать информацию в логи, то их размер получился внушительный – 22795 файлов с суммарным весом в 278 ГБ \(\!\) без сжатия\.

Сначала посмотрели выхлоп _strace_\. И сразу же увидели огромное количество вызовов [_nanosleep_](https://man7.org/linux/man-pages/man2/nanosleep.2.html)\. Это означало, что дочерние процессы, порождаемые утилитой _pvs\-studio\-analyzer_, почему\-то сидели в бесконечном ожидании\. Мы прошерстили логи сверху вниз и таки нашли проблему \(картинка кликабельная\):

![1005_StoriesFromSupport_ru/image6.gif](https://import.viva64.com/docx/blog/1005_StoriesFromSupport_ru/image6.gif)

При нажатии на картинку в анимации будет видно, что номер файлового дескриптора после открытия файлов постепенно возрастает\. После того как этот номер приблизился к значению 1024, при попытке выделения нового дескриптора была выдана ошибка _EMFILE_, а затем анализ остановился\. Такое поведение свидетельствует об утечке файловых дескрипторов\.

В ОС Linux при открытии файла ему присваивается специальный номер – дескриптор, который затем используется для работы с ним: чтения, записи, просмотра атрибутов и т\.д\. Количество таких дескрипторов ограничено и определяется настройками системы\.

Кстати, проблему воспроизвести очень легко\. Для воспроизведения проблемы достаточно написать следующий _CMakeLists\.txt_:

```cpp
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:

```cpp
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=On
pvs-studio-analyzer analyze -f ./build/compile_commands.json -j -i -o pvs.log
```

К сожалению, на момент написания статьи оригинальные логи канули в лету\. Так что на картинке выше лог с проблемой, который мы воспроизвели собственноручно\.

<details>
   <summary>Кто же был виновен?</summary>

Как уже было ранее сказано, для корректной работы инкрементального режима и проверки списка файлов требуется анализ зависимостей компилируемых файлов\. Для этого мы формируем особый файл _depend\_info\.json_, в котором отображаются зависимости компилируемых файлов от заголовочных\.

Некоторые исходные файлы могут быть многократно скомпилированы в разных проектах с разными флажками в пределах одного "решения"\. При формировании препроцессированного файла с постфиксом "\.PVS\-Studio\.i" нам приходится отрезать расширение у имени исходного файла\. Это сделано из\-за того, что некоторые препроцессоры отказываются препроцессировать файл, если итоговый содержит в имени постфиксы вроде "\.cpp", "\.cxx" и др\.

Это может привести к коллизии, если, например, препроцессируются два файла – "source\.cpp" и "source\.cxx"\. Для устранения состояния гонки мы производим блокировку результирующего пути – создаётся и открывается особый файл "\.pvslock"\. Если происходит коллизия, то к имени следующего препроцессированного файла добавится число 1, 2, 3 и т\. д\.

После первого этапа парсинга мы уже имеем все необходимые препроцессированные файлы\. Второй раз запускать препроцессор бессмысленно, и это может существенно сэкономить время последующего анализа\. Поэтому после первого этапа мы переносим объект _PreprocessedFile_, который содержит все необходимые пути, а также объекты блокировки, в кеш\. На втором этапе мы опрашиваем кеш, содержит ли он необходимый нам препроцессированный файл\.

Проблема была в том, что при перемещении объекта на первом этапе мы забывали снять блокировку\. Это приводило к росту числа открытых временных файлов "\.pvslock", и при превышении определенного числа файловых дескрипторов программа зависала\.

Правка была достаточно простой – при перемещении объекта в кеш теперь снимается блокировка и файл "\.pvslock" закрывается и уничтожается\.


</details>


Мы исправили обработку ресурсов в программе, и проблема ушла\. Подозреваем, что такая ошибка ранее ни у кого не возникала, т\. к\. Linux\-версию анализатора больше используют на сборочных серверах в обычном режиме\. Инкрементальный анализ чаще используют в связке с IDE, из которых на Linux мы полноценно поддерживаем только JetBrains CLion\. Судя по всему, до того момента не находился пользователь с необходимостью анализировать проект в инкрементальном режиме с большим количеством файлов\.

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

## Заключение

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

Надеемся, что наша история была интересна для вас\. Ну а если у вас будут какие\-либо проблемы с нашим продуктом, не стесняйтесь [обращаться](https://pvs-studio.ru/ru/about-feedback/) в нашу крутую поддержку, мы действительно поможем\.

## Похожие статьи

1. [Один день из жизни разработчика PVS\-Studio](https://pvs-studio.ru/ru/blog/posts/cpp/0842/)\.
1. [Для тех, кто хочет поиграть в детектива: найди ошибку в функции из Midnight Commander](https://pvs-studio.ru/ru/blog/posts/cpp/0610/)\.
1. [Когда дворецкий \- жертва](https://pvs-studio.ru/ru/blog/posts/0546/)\.
1. [Программные ошибки, которых не бывает](https://pvs-studio.ru/ru/blog/posts/0040/)\.
1. [Как PVS\-Studio оказался внимательнее, чем три с половиной программиста](https://pvs-studio.ru/ru/blog/posts/cpp/0587/)\.
1. [В очередной раз анализатор PVS\-Studio оказался внимательнее человека](https://pvs-studio.ru/ru/blog/posts/cpp/0582/)\.