Многие ли из вас использовали сторонние библиотеки при написании кода? Вопрос риторический, ведь без применения сторонних библиотек разработка некоторых продуктов затягивалась бы на очень-очень большое время, потому что для решения каждой проблемы приходилось бы "изобретать велосипед". Однако в использовании сторонних библиотек кроме плюсов имеются и минусы. Один из этих минусов недавно коснулся и анализатора PVS-Studio для C#. Анализатор долгое время не мог закончить анализ большого проекта из-за использования метода SymbolFinder.FindReferencesAsync из Roslyn API в диагностике V3083.

Жизнь в PVS-Studio, как обычно, шла своим чередом. Разрабатывались новые диагностики, улучшался статический анализатор, писались новые статьи. Как вдруг! У одного из пользователей нашего анализатора на его большом проекте в течение дня шёл анализ и никак не мог закончиться. Alarm! Alarm! Свистать всех наверх! И мы свистали, получили дампы от пользователя и начали разбираться в причинах долгого анализа. При подробном изучении проблемы выяснилось, что дольше всех работали 3 C# диагностики. Одной из них оказалась диагностика под номером V3083. К этой диагностике уже и раньше было повышенное внимание, но пора было предпринять конкретные действия. V3083 предупреждает о некорректных вызовах C# событий. Например, в коде:

public class IncorrectEventUse
{
  public event EventHandler EventOne;  
  protected void InvokeEventTwice(object o, Eventers args)
  {
    if (EventOne != null)
    {
      EventOne(o, args);        
      EventOne.Invoke(o, args);
    }
  }
}

V3083 укажет на вызовы обработчиков события EventOne в методе InvokeEventTwice. О причинах опасности этого кода вы можете узнать более подробно в документации этой диагностики. Со стороны, логика работы V3083 очень простая:

  • найти вызов события;

  • проверить, корректно ли это событие вызывается;

  • выдать предупреждение, если событие вызывается некорректно.

Из-за этой простоты становится еще интереснее понять причину долгой работы диагностики.

Причина замедления

На самом же деле логика чуть-чуть сложнее. V3083 в каждом файле для каждого типа создает только одно срабатывание анализатора на событие, куда записывает номера всех строк (для навигации в различных плагинах: Visual Studio, Rider, SonarQube), где событие некорректно вызывается. Получается, первым делом необходимо найти все места вызова события. Для подобной задачи в Roslyn API уже имеется метод SymbolFinder.FindReferencesAsync, который и был использован в V3083, чтобы не "изобретать велосипед".

Этот метод советуют использовать во многих руководствах: первое, второе, третье и т. д. Возможно, в каких-то простых случаях скорости работы этого метода и достаточно. Однако, чем больше кодовая база проекта, тем дольше этот метод будет работать. На 100 % мы убедились в этом только после изменения V3083.

Ускорение V3083 после изменения

При изменении кода диагностики или ядра анализатора необходимо проверить, что ничего из того, что раньше работало, не сломалось. Для этого у нас имеются позитивные и негативные тесты на каждую диагностику, юнит тесты для ядра анализатора, а также база open-source проектов (которых уже почти 90 штук). Для чего нам база open-source проектов? На ней мы запускаем наш анализатор для проверки его в "боевых условиях", а также этот прогон служит дополнительной проверкой, что мы ничего не сломали в анализаторе. У нас уже имелся прогон анализатора на этой базе до изменения V3083. Все, что нам осталось сделать, – это совершить аналогичный прогон после изменения V3083 и выяснить выигрыш во времени. Результаты нас приятно удивили. Без использования SymbolFinder.FindReferencesAsync в V3083 мы получили ускорение на тестах на 9 %. Если кому-то эти цифры показались незначительными, то вот вам характеристики компьютера, на котором производились замеры:

Думаю, после этого даже самые упертые скептики убедились в масштабах проблемы, которая тихо-мирно жила в диагностике V3083.

Заключение

Пусть всем, кто использует Roslyn API, эта заметка будет предостережением! И вы не допустите наших ошибок. Причем, это касается не только метода SymbolFinder.FindReferencesAsync, но и всех других методов класса Microsoft.CodeAnalysis.FindSymbols.SymbolFinder, которые используют один и тот же механизм.

Также советую всем разработчикам тщательней и внимательней изучать библиотеки, которые вы используете. Я говорю это не просто так! Чтобы понять, почему это так важно, советую изучить две других наших заметки: первая, вторая. В них этот вопрос рассмотрен более подробно.

Кроме диагностик мы занялись другими оптимизациями анализатора PVS-Studio, о чем расскажем в следующих статьях и заметках.

Изменение диагностики V3083 пока что не попало в релиз, поэтому версия анализатора 7.12 работает с использованием SymbolFinder.FindReferencesAsync.

Как упоминалось ранее, замедление анализатора было найдено еще в двух C# диагностиках, кроме V3083. Ради интереса, предлагаю читателям оставлять свои предположения о том, какие это диагностики в комментариях. Когда предположений станет больше 50, я приоткрою завесу тайны и назову номера этих диагностик.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Valery Komarov. Roslyn API: Why PVS-Studio Was Analyzing the Project So Long.