Для получения триального ключа
заполните форму ниже
Team License (базовая версия)
Enterprise License (расширенная версия)
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
GBP
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

>
>
>
Лучшее - враг хорошего

Лучшее - враг хорошего

09 Сен 2019

Эта статья о том, как однажды мы решили немного улучшить внутренний инструмент SelfTester, применяемый для проверки качества работы анализатора PVS-Studio. Улучшение было несложным и выглядело полезным, но создало нам много проблем, и впоследствии выяснилось, что лучше бы мы его не делали.

0667_Great_Enemy_of_Good_ru/image1.png

SelfTester

Мы разрабатываем и продвигаем статический анализатор кода PVS-Studio для языков C, C++, C# и Java. Для проверки качества работы анализатора мы используем внутренние инструменты, которые носят общее название SelfTester. Для каждого из поддерживаемых языков была создана своя версия SelfTester. Это связано с особенностями тестирования, да и так просто удобнее. Таким образом, на данный момент у нас в компании используется три внутренних инструмента SelfTester для C\C++, C# и Java, соответственно. Далее я буду рассказывать про Windows версию SelfTester для C\C++ Visual Studio проектов, называя его просто SelfTester. Этот тестер был первым в линейке подобных внутренних инструментов, он является наиболее продвинутым и наиболее сложным из всех.

Как работает SelfTester? Идея проста: взять пул тестовых проектов (мы используем реальные проекты с открытым исходным кодом) и выполнить их анализ при помощи PVS-Studio. В результате для каждого проекта генерируется лог предупреждений анализатора. Этот лог сравнивается с эталонным логом для этого же проекта. В ходе сравнения логов SelfTester создает журнал сравнения логов в удобном для восприятия разработчиком виде.

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

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

Итак, задача SelfTester - работа с пулом тестовых проектов (кстати, для С/C++ их уже более 120). Проекты для пула отобраны в виде решений Visual Studio. Сделано это для того, чтобы дополнительно проверять работу анализатора на разных версиях Visual Studio, которые поддерживает анализатор (от Visual Studio 2010 до Visual Studio 2019 на данный момент).

Примечание: далее я буду разделять понятия решение и проект, понимая проект как часть решения, как это принято в Visual Studio.

Интерфейс SelfTester имеет вид:

0667_Great_Enemy_of_Good_ru/image2.png

Слева представлен список решений, справа - результаты проверки для каждой версии Visual Studio.

Серые метки "Not supported" говорят о том, что решение не поддерживает выбранную версию Visual Studio или не было сконвертировано для этой версии. У некоторых решений в пуле есть настройка, указывающая конкретную версию Visual Studio для проверки. Если же версия не задана, то решение будет обновлено до всех последующих версий Visual Studio. Пример такого решения на скриншоте - "smart_ptr_check.sln" (проверка выполнена для всех версий Visual Studio).

Зеленая метка "OK" сигнализирует, что очередная проверка не выявила различий с эталонным логом. Красная метка "Diff" свидетельствует о наличии различий. Именно на такие метки следует обратить внимание разработчику. Для этого ему необходимо дважды кликнуть мышью на необходимую метку. Выбранное решение будет открыто в нужной версии Visual Studio, также там будет открыто окно с логом предупреждений. Кнопки управления внизу позволяют повторно запускать анализ выбранного или всех решений, назначать выбранный лог (или сразу все) эталонными и т.п.

Представленные результаты работы SelfTester всегда дублируются в html-отчет (журнал различий).

Помимо GUI у SelfTester есть и автоматизированные режимы работы для запуска в ходе ночных сборок. Однако обычный паттерн использования - неоднократные запуски разработчиком в течение рабочего дня. Поэтому одна из важных характеристик для SelfTester - скорость работы.

Почему важна скорость работы:

  • Для запуска в ходе ночных тестов достаточно критично время выполнения каждого шага. Очевидно, что чем быстрее проходят тесты - тем лучше. А среднее время работы SelfTester в настоящее время превышает 2 часа;
  • При запуске SelfTester в течение дня разработчику приходится меньше ждать результата, что увеличивает производительность труда.

Именно желание ускорить работу SelfTester стало причиной доработок в этот раз.

Многопоточность в SelfTester

SelfTester изначально создавался как многопоточное приложение с возможностью параллельной проверки нескольких решений. Единственное ограничение состояло в том, что нельзя параллельно проверять одно и то же решение под разные версии Visual Studio, так как многие решения необходимо обновлять до определенных версий Visual Studio перед проверкой. В ходе этого вносятся изменения непосредственно в файлы проектов .vcxproj, что приводит к ошибкам при параллельном запуске.

Чтобы сделать работу более эффективной, в SelfTester использован интеллектуальный планировщик заданий, позволяющий задавать строго лимитированное значение параллельных потоков и поддерживать его.

Планировщик используется на двух уровнях. Первый - уровень решений, используется для запуска проверки решения .sln при помощи утилиты PVS-Studio_Cmd.exe. Внутри PVS-Studio_Cmd.exe (на уровне проверки файлов исходного кода) используется тот же самый планировщик, но с другой настройкой степени параллелизма.

Степень параллелизма - параметр, фактически указывающий, сколько параллельных потоков должно выполняться одновременно. Для значений степени параллелизма уровня решений и файлов были выбраны значения по умолчанию четыре и восемь, соответственно. Таким образом, число параллельных потоков при данной реализации должно быть равным 32 (четыре одновременно проверяемых решения и восемь файлов). Данная настройка представляется нам оптимальной для работы анализатора на восьмиядерном процессоре.

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

Примечание: далее будем считать, что работа ведется со значениями степени параллелизма по умолчанию.

Планировщик LimitedConcurrencyLevelTaskScheduler унаследован от System.Threading.Tasks.TaskScheduler и доработан для обеспечения максимального уровня параллелизма при работе поверх ThreadPool. Иерархия наследования:

LimitedConcurrencyLevelTaskScheduler : PausableTaskScheduler
{ .... }
PausableTaskScheduler: TaskScheduler
{ .... }

PausableTaskScheduler позволяет приостанавливать выполнение задач, а LimitedConcurrencyLevelTaskScheduler, в дополнение к этому, обеспечивает интеллектуальное управление очередью задач и планирование их выполнения, учитывая степень параллелизма, объем запланированных задач и другие факторы. Планировщик используется при запуске задач System.Threading.Tasks.Task.

Предпосылки доработок

Описанная выше реализация работы имеет недостаток: она не оптимальна при работе с решениями разного размера. А размер решений в тестовом пуле очень разный: от 8 Кб до 4 Гб для размера папки с решением, и от одного до нескольких тысяч файлов исходного кода в каждом.

Планировщик ставит решения в очередь просто по порядку, без какой-либо интеллектуальной составляющей. Напомню, что по умолчанию одновременно не может выполняться проверка более четырёх решений. Если в данный момент выполняется проверка четырёх больших решений (число файлов в каждом более восьми), то предполагается, что мы работаем эффективно, так как используем максимально возможное число потоков (32).

Но представим довольно частую ситуацию, когда проверяется несколько небольших решений. Например, одно решение большое и содержит 50 файлов (будет задействован максимум из восьми потоков), а три другие содержат по три, четыре и пять файлов каждое. В таком случае мы задействуем всего 20 потоков (8 + 3 + 4 + 5). Получаем недоиспользование процессорного времени и снижение общей производительности.

Примечание: на самом деле, узким местом, как правило, является всё же дисковая подсистема, а не процессор.

Доработки

Улучшением, которое само напрашивается в данном случае, является ранжирование списка подаваемых на проверку решений. Необходимо добиться оптимального использования заданного числа одновременно выполняемых потоков (32), подавая на проверку проекты с "правильным" числом файлов.

Рассмотрим снова наш пример, когда проверяются четыре решения со следующим числом файлов в каждом: 50, 3, 4 и 5. Задача, проверяющая решение из трёх файлов, вероятно, скоро отработает. И вот вместо неё было бы оптимально добавить решение, в котором восемь или более файлов (чтобы задействовать для этого решения максимум из восьми доступных потоков). Тогда суммарно мы будем задействовать уже 25 потоков (8 + 8 + 4 + 5). Неплохо. Тем не менее, семь потоков всё еще остались не задействованы. И вот тут возникает идея еще одной доработки, связанной со снятием ограничения на четыре потока для проверки решений. Ведь в примере выше можно добавить не одно, а несколько решений, задействовав по возможности все 32 потока. Давайте представим, что у нас есть еще два решения по три и четыре файла в каждом. Добавление этих задач полностью закроет "брешь" неиспользуемых потоков, и их будет 32 (8 + 8 + 4 + 5 + 3 + 4).

Думаю, идея понятна. На самом деле, реализация этих доработок также не потребовала много усилий. Все было сделано за один день.

Потребовалась доработка класса задачи: наследование от System.Threading.Tasks.Task и добавление поля "вес". Для установки веса решения применяется простой алгоритм: если число файлов в решении меньше восьми, то вес устанавливается равным этому значению (например, 5), если же число файлов более или равно восьми, то вес выбирается равным восьми.

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

Наконец, потребовался предварительный шаг, на котором производился анализ всех решений пула (эвалюация с помощью MSBuild API) для вычисления и установки весов решений (получения числа файлов с исходным кодом).

Результат

Думаю, после такого длинного вступления вы уже догадались, что результат оказался нулевым.

0667_Great_Enemy_of_Good_ru/image4.png

Хорошо хоть, что доработки были несложными и быстрыми.

Ну а теперь, собственно, начинается часть статьи про "создало нам много проблем" и вот это вот всё.

Побочные эффекты

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

Тем не менее, доработку было решено оставить как "не мешающую" и потенциально полезную. К тому же, пул тестовых решений постоянно пополняется, поэтому в будущем, возможно, ситуация изменится.

И тут...

0667_Great_Enemy_of_Good_ru/image5.png

Один из разработчиков пожаловался на "падение" SelfTester. Ну, бывает. Чтобы эта ошибка не затерялась, был заведен внутренний инцидент (тикет) с именем "Исключение при работе с SelfTester". Ошибка происходила при эвалюации проекта. Правда, такое обилие окошек свидетельствовало о проблеме еще и в обработчике ошибок. Но это было быстро устранено, и в течение следующей недели ничего не ломалось. Как вдруг, уже другой пользователь пожаловался на SelfTester. И снова на ошибку эвалюации проекта:

0667_Great_Enemy_of_Good_ru/image7.png

Стек в этот раз содержал больше полезной информации - ошибка в формате xml. Вероятно, при обработке файла проекта Proto_IRC.vcxproj (его xml-представления) что-то случилось с самим файлом, поэтому XmlTextReader не смог его обработать.

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

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

Важный момент: код, на котором проявлялась ошибка, был переиспользован в SelfTester. Изначально он применяется для эвалюации проектов в самом анализаторе (PVS-Studio_Cmd.exe). Именно поэтому внимание к проблеме выросло. Однако подобных падений в анализаторе не происходило.

Тем временем тикет про проблемы с SelfTester пополнялся новыми ошибками:

0667_Great_Enemy_of_Good_ru/image8.png

И снова XmlException. Очевидно, что где-то есть соперничающие потоки, работающие с файлами проектов на чтение и запись. SelfTester работает с проектами в следующих случаях:

  • Эвалюация проектов в ходе предварительного вычисления весов решений: новый шаг, который изначально вызвал подозрения;
  • Обновление проектов до нужных версий Visual Studio: выполняется непосредственно перед проверкой (проекты никак не пересекаются) и не должно оказывать влияния на работу;
  • Эвалюация проектов при проверке: отлаженный потокобезопасный механизм, который переиспользован из PVS-Studio_Cmd.exe;
  • Восстановление файлов проектов (замена изменённых файлов .vcxproj изначальными эталонными файлами) при выходе из SelfTester, так как файлы проектов в процессе работы могут обновляться до нужных версий Visual Studio: завершающий шаг, также не оказывающий влияния на другие механизмы.

Подозрение пало на новый код, добавленный для оптимизации (вычисление весов). Но изучение этого кода показало, что если пользователь запускал анализ сразу после старта SelfTester, то тестер всегда корректно дожидался окончания предварительной эвалюации. Это место выглядело безопасно.

Снова нам не удалось выявить источник проблемы.

Боль

Весь следующий месяц SelfTester продолжал время от времени падать. Тикет пополнялся данными, вот только непонятно было, что с этими данными делать. Большинство падений было всё с тем же исключением XmlException. Изредка было что-то другое, но на том же переиспользованном коде из PVS-Studio_Cmd.exe.

0667_Great_Enemy_of_Good_ru/image10.png

По традиции к внутренним инструментам предъявляются не такие высокие требования, поэтому всё же работа с ошибками SelfTester велась по остаточному принципу. Время от времени подключались разные люди (за все время инцидента над проблемой поработало шесть человек, включая двух практикантов-стажёров). Тем не менее, на задачу приходилось отвлекаться.

Первая наша ошибка. На самом деле, вот тут уже можно было решить проблему раз и навсегда. Как? Было понятно, что ошибка вызвана новой оптимизацией. Ведь до неё все работало хорошо, да и переиспользованный код явно не может быть настолько плох. К тому же, эта оптимизация не принесла никакой выгоды. Так что же надо было сделать? Удалить эту оптимизацию. Как вы понимаете, сделано это не было. Мы продолжили работать над проблемой, которую сами же себе и создали. Продолжался поиск ответа на вопрос: "КАК???" Как оно падает? Всё же вроде написано правильно.

Вторая наша ошибка. К решению проблемы были подключены другие люди. Очень, очень большая ошибка. К сожалению, это не только не решило проблему, но и были затрачены дополнительные ресурсы. Да, новые люди принесли новые идеи, но для их реализации потребовалось затрачивать (абсолютно впустую) много рабочего времени. На определенном этапе писались тестовые программы (теми самыми практикантами), эмулирующие эвалюацию одного и того же проекта в разных потоках с параллельной модификацией проекта в другом потоке. Не помогло. Кроме того, что мы и так знали до этого, MSBuild API потокобезопасен внутри, ничего нового не выяснили. А ещё в SelfTester было добавлено сохранение мини-дампа при возникновении исключения XmlException. Потом всё это кто-то дебажил, жуть. Проводились обсуждения, делалось много и других ненужных вещей.

Наконец, третья наша ошибка. Знаете, сколько времени прошло с момента возникновения проблемы с SelfTester и до её решения? Хотя нет, посчитайте сами. Инцидент был создан 17.09.2018 и закрыт 20.02.2019, и там более 40 (сорока!) сообщений. Ребята, это же куча времени! Мы позволили себе пять месяцев заниматься ЭТИМ. В это же самое время (параллельно) мы занимались поддержкой Visual Studio 2019, добавляли язык Java, начали внедрение стандарта MISRA C/C++, улучшали C# анализатор, активно участвовали в конференциях, писали кучу статей и т.п. И все эти работы недополучили время разработчиков из-за бестолковой ошибки SelfTester.

Граждане, учитесь на наших ошибках и никогда так не делайте. И мы не будем.

У меня все.

0667_Great_Enemy_of_Good_ru/image11.png

Конечно, это шутка, и я расскажу, в чём же состояла проблема с SelfTester :)

Бинго!

К счастью, среди нас оказался человек с наименее замутнённым сознанием (мой коллега Сергей Васильев), который просто взглянул на проблему под совсем другим углом (а ещё - ему немного повезло). Что, если внутри SelfTester действительно всё в порядке, а проекты ломает что-то извне? Параллельно с SelfTester обычно ничего не запускалось, в ряде случаев мы строго контролировали среду выполнения. В таком случае этим "что-то" мог быть только сам SelfTester, но другой его экземпляр.

При выходе из SelfTester, поток, восстанавливающий проектные файлы из эталонов, какое-то время продолжает работу. В этот момент возможен повторный запуск тестера. Защита от одновременного запуска нескольких экземпляров SelfTester была добавлена позже и сейчас выглядит так:

0667_Great_Enemy_of_Good_ru/image12.png

Но тогда её не было.

Невероятно, но за почти полгода мучений никто не обратил на это внимания. Восстановление проектов из эталонов - достаточно быстрая фоновая процедура, но, к сожалению, недостаточно быстрая, чтобы не мешать при повторном запуске SelfTester. А что у нас происходит при запуске? Правильно, вычисление весов решений. Один процесс перезаписывает файлы .vcxproj, в то время как другой пытается их прочитать. Привет, XmlException.

Сергей выяснил всё это, когда добавлял в тестер возможность переключения на режим работы с другим набором эталонных логов. Необходимость в этом возникла после добавления в анализатор набора правил MISRA. Переключаться можно непосредственно в интерфейсе, при этом пользователь видит окошко:

0667_Great_Enemy_of_Good_ru/image13.png

После чего SelfTester перезапускается. Ну а ранее, видимо, пользователи как-то сами эмулировали проблему, запуская тестер повторно.

Разбор полётов и выводы

Конечно, мы удалили, а точнее, отключили созданную ранее оптимизацию. К тому же, это было гораздо проще, чем делать какую-то синхронизацию между повторными запусками тестера самим собой. И все начало отлично работать, как и раньше. А в качестве дополнительной меры была добавлена описанная выше защита от одновременного запуска тестера.

Про наши основные ошибки в ходе поиска проблемы я уже написал выше, так что хватит самобичевания. Мы тоже люди, а поэтому ошибаемся. Важно учиться на своих ошибках и делать выводы. Выводы тут достаточно просты:

  • Нужно отслеживать и оценивать рост сложности задачи;
  • Вовремя останавливаться;
  • Пробовать взглянуть на проблему более широко, так как со временем взгляд "замыливается", а угол зрения - сужается;
  • Не бояться удалять старый или ненужный код.

Вот теперь точно - всё. Спасибо, что дочитали. Всем безбажного кода!

Популярные статьи по теме
Бесплатный PVS-Studio для тех, кто развивает открытые проекты

Дата: 22 Дек 2018

Автор: Андрей Карпов

В канун празднования нового 2019 года команда PVS-Studio решила сделать приятный подарок всем контрибьюторам open-source проектов, хостящихся на GitHub, GitLab или Bitbucket. Им предоставляется возмо…
Как и почему статические анализаторы борются с ложными срабатываниями

Дата: 20 Мар 2017

Автор: Андрей Карпов

В своей предыдущей статье я писал, что мне не нравится подход, при котором статические анализаторы кода оцениваются с помощью синтетических тестов. В статье приводился пример, воспринимаемый анализат…
Эффект последней строки

Дата: 31 Май 2014

Автор: Андрей Карпов

Я изучил множество ошибок, возникающих в результате копирования кода. И утверждаю, что чаще всего ошибки допускают в последнем фрагменте однотипного кода. Ранее я не встречал в книгах описания этого …
Характеристики анализатора PVS-Studio на примере EFL Core Libraries, 10-15% ложных срабатываний

Дата: 31 Июл 2017

Автор: Андрей Карпов

После большой статьи про проверку операционной системы Tizen мне было задано много вопросов о проценте ложных срабатываний и о плотности ошибок (сколько ошибок PVS-Studio выявляет на 1000 строк кода)…
Любите статический анализ кода!

Дата: 16 Окт 2017

Автор: Андрей Карпов

Я в шоке от возможностей статического анализа кода, хотя сам участвую в разработке инструмента PVS-Studio. На днях я был искренне удивлён тому, что анализатор оказался умнее и внимательнее меня.
Статический анализ как часть процесса разработки Unreal Engine

Дата: 27 Июн 2017

Автор: Андрей Карпов

Проект Unreal Engine развивается - добавляется новый код и изменятся уже написанный. Неизбежное следствие развития проекта - появление в коде новых ошибок, которые желательно выявлять как можно раньш…
PVS-Studio для Java

Дата: 17 Янв 2019

Автор: Андрей Карпов

В седьмой версии статического анализатора PVS-Studio мы добавили поддержку языка Java. Пришло время немного рассказать, как мы начинали делать поддержку языка Java, что у нас получилось и какие дальн…
PVS-Studio ROI

Дата: 30 Янв 2019

Автор: Андрей Карпов

Время от времени нам задают вопрос, какую пользу в денежном эквиваленте получит компания от использования анализатора PVS-Studio. Мы решили оформить ответ в виде статьи и привести таблицы, которые по…
Зло живёт в функциях сравнения

Дата: 19 Май 2017

Автор: Андрей Карпов

Возможно, читатели помнят мою статью под названием "Эффект последней строки". В ней идёт речь о замеченной мной закономерности: ошибка чаще всего допускается в последней строке однотипных блоков текс…
Как PVS-Studio оказался внимательнее, чем три с половиной программиста

Дата: 22 Окт 2018

Автор: Андрей Карпов

PVS-Studio, как и другие статические анализаторы кода, часто выдаёт ложные срабатывания. Но не стоит спешить считать странные срабатывания ложными. Это короткая история о том, как PVS-Studio вновь ок…

Комментарии (0)

Следующие комментарии

На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Этот сайт использует куки и другие технологии, чтобы предоставить вам более персонализированный опыт. Продолжая просмотр страниц нашего веб-сайта, вы принимаете условия использования этих файлов. Если вы не хотите, чтобы ваши данные обрабатывались, пожалуйста, покиньте данный сайт. Подробнее →
Принять