Май 2015 г.

Том 30, выпуск 5


Visual Studio 2015 - Анализ производительности в процессе отладки в Visual Studio 2015

Dan Taylor | Май 2015

Продукты и технологии:

Visual Studio 2015 RC

В статье рассматриваются:

  • новое отладочное окно в Visual Studio 2015;
  • улучшение производительности приложения при отладке;
  • отслеживание увеличения объема занимаемой памяти и утечек.

Исходный код можно скачать по ссылке

Многие разработчики тратят большую часть своего времени на то, чтобы заставить приложение правильно работать. Соответственно меньше времени уделяется производительности приложения. Хотя в Visual Studio уже некоторое время существуют средства профилирования, они всегда были отдельным набором инструментов, которыми надо учиться пользоваться. И зачастую разработчики не тратили время на их освоение, а использовали по наитию, только когда возникали явные проблемы с производительностью.

В этой статье мы ознакомим читателей с новым окном отладчика — Diagnostic Tools — в Visual Studio 2015. Кроме того, будет рассказано, как с его помощью анализировать производительность прямо в ходе рабочего процесса отладки. Сначала мы дадим обзор средств и возможностей отладчика, а затем углубимся в детали. Я покажу, как использовать PerfTips для замера времени выполнения разделов кода между точками прерывания и шагами при отладке, как с помощью окна Diagnostic Tools отслеживать нагрузку на процессор и память и как делать снимки системы, чтобы анализировать утечки и рост использования памяти.

Средства, описываемые в этой статье, доступны для отладки большинства управляемых и неуправляемых проектов. Microsoft постоянно расширяет поддержку на большее количество типов проектов и отладочных конфигураций. Текущую актуальную информацию о поддерживаемых средствах см. в публикации блога по окну Diagnostic Tools на aka.ms/diagtoolswindow. Отдельная статья в этом номере посвящена тому, как пользоваться IntelliTrace в окне Diagnostic Tools (см. статью «Применение IntelliTrace для ускорения диагностики»), чтобы быстро выявлять корневые причины ошибок в вашем коде.

Производительность при отладке

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

  1. Вставить в приложение код (например, System.Diagnostics.Stopwatch), который замеряет время выполнения между различными точками, итеративно добавляя контрольные таймеры (stopwatches) по мере необходимости для сокращения «горячего» пути (hot path).
  2. Пошагово выполнять код, чтобы увидеть, не «тормозит» ли код на каком-то из шагов.
  3. Нажимать кнопку Break All («пауза») в случайных точках, чтобы получить представление о том, насколько далеко продвинулось выполнение. В некоторых кругах это называют «дешевым вариантом выборки» (poor man’s sampling).
  4. Избыточная оптимизация (over-optimize) кода вообще безо всяких замеров производительности, иногда путем применения набора рекомендаций по увеличению производительности всей кодовой базы.

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

Окно Diagnostic Tools Основное отличие, которое вы заметите при отладке кода в Visual Studio 2015, — новое окно Diagnostic Tools, которое появится, как показано на рис. 1. Diagnostic Tools предоставляет информацию двумя способами, дополняющими друг друга. В верхней половине окна рисуются графики по временной оси, а более подробная информация сообщается на вкладках в нижней части окна.

Новое окно Diagnostic Tools в Visual Studio 2015
Рис. 1. Новое окно Diagnostic Tools в Visual Studio 2015

В Visual Studio 2015 вы увидите три инструмента в окне Diagnostics Tools: Debugger (включает IntelliTrace), Memory Usage и CPU Usage. Вы можете включать и отключать инструменты CPU Usage и Memory Usage, щелкая раскрывающийся список Select Tools. Инструмент Debugger имеет три дорожки, на которых показываются Break Events (события прерывания), Output Events (события вывода) и IntelliTrace Events (события IntelliTrace).

История Break Events и PerfTips Break Events позволяют увидеть, сколько времени ушло на выполнение каждого раздела кода. Прямоугольники представляют длительность от момента запуска приложения или возобновления выполнения до момента, когда Debugger поставил код на паузу (рис. 2).

Break Events и PerfTips
Рис. 2. Break Events и PerfTips

Начало прямоугольника указывает, где вы запустили приложение, используя команды Continue (F5), Stepping (F10, F11, Shift+F11) или Run-to-cursor (Ctrl+F10), а конец прямоугольника — где приложение было остановлено из-за достижения точки прерывания, завершения шага или команды Break All.

Основное отличие, которое вы заметите при отладке кода в Visual Studio 2015, — новое окно Diagnostic Tools.

Длительность самого недавнего Break Event также показывается в коде в конце текущей строки на вкладке Debugger. Это называют PerfTips. Они дают возможность следить за производительностью, не отрывая глаз от кода.

В таблице подробностей под графиком вы также можете увидеть историю и длительность Break Events и PerfTips в табличном формате. Если вы работаете с IntelliTrace, в этой таблице отображаются дополнительные события. С помощью фильтра можно просматривать только данные Debugger, чтобы видеть лишь историю Break Events.

Анализ использования процессора и памяти На временной оси автоматически выделяются временные диапазоны по мере того, как вы устанавливаете точки прерывания и делаете шаг в выполнении. Когда вы достигаете точки прерывания, текущий временной диапазон сбрасывается и отображает только самое недавнее Break Event. Это выделение можно расширить, чтобы включить последнее Break Event. Вы можете переопределить автоматическое выделение временного диапазона, щелкнув прямоугольник Break Event или щелкнув и перетащив его по временной оси.

Выделение временного диапазона позволяет вам соотносить диапазоны на графиках CPU Usage и Memory Usage, чтобы вы могли понять характеристики использования процессора и памяти конкретными разделами кода. Пока приложение выполняется, графики продолжают обновляться, что позволяет отслеживать использование процессора и памяти при вашем взаимодействии с приложением. Вы можете переключиться на вкладку Memory Usage, чтобы сделать снимки и просмотреть детальную раскладку использования памяти.

IntelliTrace Это средство (недоступное в Community-версии Visual Studio) позволяет глубже изучить производительность при отладке управляемого кода. IntelliTrace добавляет две дорожки на временную ось Debugger Events: Output и IntelliTrace. Эти события включают информацию, выводимую в окно Output, плюс дополнительные события, собираемые IntelliTrace, такие как Exceptions, ADO.NET и др. СОбытия, фиксируемые на этих дорожках, также показываются в Debugger Events Table.

Вы можете соотносить события IntelliTrace с пиками на графиках CPU Usage и Memory Usage. Временные отметки сообщают, сколько времени занимают различные операции в вашем приложении. Например, в код можно добавить выражения Debug.WriteLine и использовать временные метки в событиях Output, чтобы увидеть, сколько времени ушло на выполнение от одного выражения до следующего.

Улучшение производительности и использования памяти

Теперь, когда мы посмотрели возможности нового окна, займемся практическим применением инструментария. В этом разделе мы будем решать набор проблем с производительностью в приложении-примере PhotoFilter. Это приложение скачивает картинки из облака и загружает их в локальную библиотеку картинок пользователя, чтобы тот мог просматривать их и применять к ним различные фильтры.

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

Медленный запуск приложения Начав отладку приложения PhotoFilter, вы обнаружите, что оно долго запускается и загружает картинки. Это очевидная проблема.

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

Установите точки прерывания в начале и конце функции LoadImages (как в коде на рис. 3) и запустите отладку (F5). Когда код достигнет первой точки прерывания, нажмите Continue (F5), чтобы продолжить выполнение до второй точки прерывания. Теперь на временной оси Debugger Events присутствуют два события прерывания.

Метод LoadImages

Метод LoadImages
Рис. 3. Метод LoadImages

Первый шаг показывает, что приложение выполнялось всего 274 мс до достижения первой точки прерывания. Второй — что на выполнение кода в функции LoadImages до достижения второй точки прерывания ушло 10 476 мс. Кроме того, такое же значение видно прямо в коде — оно отображается с помощью PerfTip. Итак, вы сузили проблему до функции LoadImages.

Чтобы получить больше информации и узнать время выполнения каждой строки кода, перезапустите отладку, чтобы снова попасть в первую точку прерывания. На этот раз выполняйте каждую строку в методе пошагово, чтобы увидеть, какие из них долго выполняются. Из PerfTips и длительности отладочных событий прерывания видно, что GetImagesFromCloud требует на выполнение 7290 мс, LoadImagesFromDisk — 736 мс, LINQ-запрос — 1322 мс, а остальные строки выполняются менее чем за 50 мс каждая.

Показатели времени для всех строк представлены на рис. 4. При этом выводятся номера строк, и, например, строка 52 сообщает, сколько времени ушло на выполнение строки 51. Теперь углубимся в метод GetImagesFromCloud.

Таблица событий отладчика показывает время выполнения каждого шага
Рис. 4. Таблица событий отладчика показывает время выполнения каждого шага

Метод GetImagesFromCloud выполняет две логически разные операции (рис. 5). Он скачивает список картинок с сервера и эскизы (thumbnails) для каждой картинки в синхронном режиме (по одной за раз). Вы можете замерить время этих операций, удалив текущие точки прерывания и поместив новые в следующие строки:

line 63: HttpClient client = new HttpClient();
line 73: foreach (var image in pictureList)
line 79: }

Метод GetImagesFromCloud улучшенный код

Рис. 5. Метод GetImagesFromCloud (вверху) и улучшенный код (внизу)

Перезапустите процесс отладки и подождите, когда код не достигнет первой точки прерывания. Затем продолжите выполнение (нажав F5) до второй точки прерывания. Это позволит приложению извлечь список картинок из облака. Далее пусть приложение выполняется до второй точки прерывания, чтобы замерить длительность скачивания эскизов из облака. PerfTips и Break Event сообщат вам, что на получение списка картинок ушло 565 мс, а на скачивание эскизов — 6426 мс. Бутылочное горлышко для производительности — операция скачивания эскизов.

Если вы посмотрите на график CPU Usage (рис. 6), когда метод извлекает список изображений, то заметите, что использование процессора сравнительно велико. Этот же график почти плоский при скачивании эскизов, а значит, процесс тратит много времени в ожидании сетевого ввода-вывода.

График CPU Usage указывает на задержки в сетевом вводе-выводе
Рис. 6. График CPU Usage указывает на задержки в сетевом вводе-выводе

Чтобы свести к минимуму время ожидания полных циклов обмена данными между клиентом и сервером, начинайте все операции скачивания эскизов одновременно и асинхронно и ожидайте их окончания через await, используя .NET System.Tasks. Замените строки 73–79 (из кода на рис. 5) на следующее:

// Скачивание эскизов
var downloadTasks = new List<Task>();
foreach (var image in pictureList)
{
  string fileName = image.Thumbnail;
  string imageUrl = ServerUrl + "/Images/" + fileName;
  downloadTasks.Add(DownloadImageAsync(new Uri(imageUrl),
    folder, fileName));
}
await Task.WhenAll(downloadTasks);

Замерив выполнение новой версии, вы увидите, что она требует всего 2424 мс. Улучшение почти на четыре секунды.

Отладка разрастания использования памяти и утечек Если вы следили за графиком Memory Usage при диагностировании медленного запуска, то, вероятно, заметили резкое увеличение в использовании памяти при старте приложения. Список эскизов является виртуализованным, и единовременно отображается только одно полноразмерное изображение. Одно из преимуществ применения виртуализованного списка в том, что он загружает лишь тот контент, который показывается на экране, поэтому не следовало бы ожидать одномоментного появления множества эскизов в памяти.

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

График Memory Usage дает высокоуровневое представление того, как ваше приложение использует память. Есть счетчик производительности Private Bytes для приложения. Private Bytes — это мера общего объема памяти, выделенной процессу. Он не включает память, общую с другими процессами. Он учитывает управляемую и неуправляемую кучи, стеки потоков и другую память (например, закрытые разделы загруженных файлов .dll).

При разработке нового приложения или диагностике проблемы с существующей программой неожиданный рост на графике Memory Usage зачастую является первым признаком того, что у вас есть код, который ведет себя не так, как задумывалось. Наблюдая за этим графиком, вы можете использовать такие средства отладчика, как точки прерывания и пошаговое выполнение кода, чтобы сузить область поисков некорректного кода. По номерам строк и длительности операций, показанным на вкладке Debugger Events на рис. 4, можно определить, что за неожиданный рост отвечает строка 52, где вызывается метод LoadImagesFromDisk. Зачастую следующим шагом в нахождении неожиданного увеличения использования памяти является создание снимков. На вкладке Memory Usage щелкните кнопку Take Snapshot, чтобы сгенерировать снимок кучи. Вы можете создавать снимки в точках прерывания или в ходе выполнения приложения.

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

Затем выполните метод LoadImagesFromDisk и создайте другой снимок. Теперь, сравнив снимки, вы сможете понять, какие управляемые типы были добавлены в кучу в результате вызова функции. График вновь показывает всплеск в использовании памяти (рис. 7). Задержав курсор мыши над графиком, вы можете увидеть, что объем занимаемой памяти был 47,4 Мб. Будет неплохо отметить себе это число, чтобы впоследствии проверить оказало ли ваше исправление значимое влияние.

Заметный всплеск в использовании памяти
Рис. 7. Заметный всплеск в использовании памяти

Режим просмотра подробностей дает краткую сводку по каждому снимку. В сводку включаются последовательный номер снимка, время выполнения (в секундах) на момент создания снимка, размер кучи и количество существующих объектов в куче. Последующие снимки также отразят изменения в размере и количестве объектов по сравнению с предыдущим снимком.

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

Размер кучи, отображаемый в сводке снимка, будет меньше, чем в Private Bytes, показываемом на графике Memory Usage. Сводка Private Bytes отражает все типы памяти, выделенные вашим процессом, а снимок — размер всех используемых объектов в управляемой куче. Если вы наблюдаете большое увеличение на графике Memory Usage, но рост объема занимаемой памяти в управляемой куче несравним с ним, значит, рост происходит где-то в другой области памяти.

Из сводки снимка можно открыть Heap View и проанализировать содержимое кучи по типам. Щелкните ссылку diff в столбце Objects (Diff) для вашего второго снимка, чтобы открыть Heap View в новой вкладке. Щелчок этой ссылки приводит к сортировке типов в Heap View по количеству новых объектов, созданных с момента последнего снимка. Это собирает интересующие вас типы в начале таблицы.

С помощью графика Memory можно отслеживать, как ваше приложение использует память в процессе отладки.

В Heap View Snapshot (рис. 8) два основных раздела: таблица Object Type в верхней панели и References Graph в нижней. В таблице Object Type перечисляются имя, количество и размер объекта каждого типа на момент создания снимка.

Несколько типов в Heap View исходят от инфраструктуры. Если вы включили Just My Code (по умолчанию), это типы, которые ссылаются на типы в вашем коде или на которые ссылаются ваши типы. Используя это представление, вы можете обнаружить тип из своего кода — PhotoFilter.ImageItem; он находится ближе к началу таблицы.

На рис. 8 видно, что в столбце Count Diff показывается 137 новых объектов ImageItem, созданных с момента предыдущего снимка. Верхние пять новых типов объектов имеют одинаковое количество новых объектов, поэтому они скорее всего взаимосвязаны.

Heap View Snapshot в режиме отображения различий
Рис. 8. Heap View Snapshot в режиме отображения различий

Посмотрим на вторую панель, References Graph. Если вы ожидаете, что тип будет очищен сборщиком мусора, но он все еще показывается в таблице типов, то отследить, что удерживает ссылку на него поможет Paths to Root. Это одно из двух представлений графика References. Paths to Root — это дерево снизу вверх, отражающее полный граф типов, корнем для которых является выбранный вами тип. Объект является корневым, если другой активный объект удерживает на него ссылку. Ненужные корневые объекты часто являются причиной утечек памяти в управляемом коде.

Второе представление — Referenced Types — является противоположностью первому. Для типа, выделенного в таблице Object Type, это представление показывает, на какие другие типы ссылается выбранный вами тип. Эта информация может быть полезна при определении того, почему объекты выделенного типа удерживают больше памяти, чем ожидалось. Это как раз весьма полезно в данном случае, поскольку типы могут использовать больше памяти, чем ожидалось, но они почему-то не устаревают.

Выберите строку PhotoFilter.ImageItem в таблице Object Type. График References обновится и покажет график для ImageItem. В представлении Referenced Types можно увидеть, что объекты ImageItem удерживают всего 280 объектов String и по 140 каждого из трех инфраструктурных типов: StorageFile, StorageItemThumbnail и BitmapImage.

По столбцу Total Size вроде бы получается, что объекты String вносят наибольший вклад в увеличение объема памяти, удерживаемой объектами ImageItem. Изучение столбца Total Size Diff имеет смысл, но сам по себе этот показатель не приведет вас к корневой причине. Некоторые инфраструктурные типы наподобие BitmapImage занимают лишь малое количество общей памяти, выделенной в управляемой куче. Количество экземпляров BitmapImage уже о чем-то говорит. Вспомните, что список эскизов в PhotoFilter виртуализован, поэтому он должен загружать эти изображения по требованию и делать их доступными сборщику мусора по завершении работы с ними. Однако получается так, словно все эскизы загружаются заранее. В сочетании с тем, что вам известно об объектах BitmapImage, которые являются верхушкой айсберга, мы продолжим анализ, не забывая об этих объектах.

Щелкните правой кнопкой мыши PhotoFilter.ImageItem в References Graph и выберите Go To Definition, чтобы открыть файл исходного кода для ImageItem в редакторе. В ImageItem определено поле m_photo, которое является BitmapImage, как показано на рис. 9.

Код, ссылающийся на поле m_photo
Рис. 9. Код, ссылающийся на поле m_photo

Первый путь кода (code path), который ссылается на m_photo, — get-аксессор свойства Photo, которое связано с ListView эскизов в UI. Здесь BitmapImage загружается по требованию (а значит, декодируется в неуправляемой куче).

Второй путь кода, ссылающийся на m_photo, — функция LoadImageFromDisk. Она находится в стартовом пути приложения. И вызывается для каждого выводимого изображения, когда приложение стартует. В конечном счете это приводит к предварительной загрузке всех объектов BitmapImage. Это поведение работает против виртуализованного ListView, так как вся память уже выделена независимо от того, отображает ListView эскиз изображения или нет. Этот алгоритм предварительной загрузки плохо масштабируется. Чем больше изображений в вашей библиотеке Pictures, тем выше издержки по памяти при запуске приложения. Загрузка объектов BitmapImage по требованию — более масштабируемое решение.

После остановки отладчика удалите комментарий со строк 81 и 82 в LoadImageFromDisk, который загружает экземпляр BitmapImage. Чтобы проверить, что вы исправили проблему с памятью, не разрушив функциональность приложения, запустите тот же эксперимент заново.

Нажмите F5 и вы увидите на графике, что общий объем используемой памяти составляет всего 26,7 Мб (рис. 10). Сделайте другой набор снимков до и сразу после вызова LoadImagesFromDisk, а затем оцените различия между ними. Вы обнаружите, что объектов ImageItem по-прежнему 137, но никаких BitmapImage нет (рис. 11). Объекты BitmapImage будут загружаться по требованию, как только вы позволите приложению продолжить запуск.

График Memory после исправления проблему со ссылками
Рис. 10. График Memory после исправления проблему со ссылками

График References после исправления проблемы с памятью
Рис. 11. График References после исправления проблемы с памятью

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

  • отладчик только управляемого кода создает снимки лишь управляемой кучи;
  • отладчик только неуправляемого кода (вариант по умолчанию для проектов с неуправляемым кодом) создает снимки лишь неуправляемой кучи;
  • отладчик смешанного режима создает снимки как управляемой, так и неуправляемой куч.

Вы можете настраивать этот параметр на странице Debug в свойствах вашего проекта.

Когда запускать инструменты без отладки

Важно отметить, что при замере производительности в отладчике вносятся дополнительные издержки. Основные издержки связаны с тем, что вы обычно выполняете сборку Debug приложения. А приложение, которое вы будете распространять для пользователей, будет сборкой Release.

В сборке Debug компилятор старается создать исполняемый файл как можно ближе к исходному коду в плане структуры и поведения. При отладке все должно работать так, как вы ожидаете. С другой стороны, в сборке Release компилятор пытается оптимизировать код ради максимальной производительности такими способами, которые сильно затрудняют отладку. Некоторые примеры включают подстановку тел функций вместо их вызовов и констант вместо переменных, удаление неиспользуемых переменных и путей кода, а также такое хранение информации о переменных, из-за чего она может оказаться нечитаемой для отладчика.

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

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

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

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

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

Заключение

Вы можете опробовать новое отладочное окно Diagnostic Tools в Visual Studio 2015, скачав Visual Studio 2015 RC. Применение этих новых интегрированных в отладчик средств может помочь вам улучшить производительность приложений в процессе их отладки.


Dan Taylorменеджер программ в группе Visual Studio Diagnostics, в последние два года работал над средствами профилирования и диагностики. До присоединения к группе Visual Studio Diagnostics был менеджером программ в группе .NET Framework и внес свой вклад во множество усовершенствований, нацеленных на повышение производительности .NET Framework и CLR.

Charles Willis  был менеджером программ в группе Visual Studio Diagnostics с момента перехода в Microsoft в прошлом году.

Выражаем благодарность за рецензирование статьи экспертам Microsoft Эндрю Холлу (Andrew Hall), Дэниэлу Моту (Daniel Moth) и Ангелосу Пертопулосу (Angelos Petropoulos).