ICorProfilerCallback :: ClassUnloadStarted не вызывается для универсального класса, даже если класс был выгружен - PullRequest
0 голосов
/ 26 февраля 2019

В настоящее время я отлаживаю профилировщик CLR моей компании (через ASP.NET 4.7.3282.0, .NET Framework 4.7.2) и вижу сценарий, в котором CLR выгружает универсальный класс, но ClassUnloadStarted обратный вызов не вызывается.

В двух словах, наш профилировщик отслеживает загруженные классы на основе ClassID, следуя ClassLoadStarted , ClassLoadFinished и ClassUnloadStarted обратных вызовов.В какой-то момент класс выгружается (вместе с соответствующим модулем), но обратный вызов ClassUnloadStarted не вызывается для соответствующего ClassID.Следовательно, у нас остался ClassID с остановкой, и мы думаем, что класс все еще загружен.Позже, когда мы пытаемся запросить этот ClassID, CLR неудивительно падает (поскольку теперь он указывает на нежелательную память).

Мой вопрос, учитывая подробный сценарий ниже:

  • Почему ClassUnloadStarted не вызывается для моего (универсального) класса?
  • Является ли это ожидаемым поведением пограничного варианта CLR или, возможно, ошибкой CLR / Profiling API?

Я не смог найти никаких документов или рассуждений, касающихся именно этого поведения, из-за того, что ClassUnloadStarted не вызывается.Никаких подсказок я тоже не нашел в коде CoreCLR.Заранее благодарим за любую помощь!

Подробный сценарий:

Это рассматриваемый класс (IComparable(T) с T=ClassFromModuleFoo):

System/IComparable`1<ClassFromModuleFoo>

Во время работы приложения проблема проявляется после выгрузки некоторых модулей.
Вот точный поток обратных вызовов загрузки / выгрузки, основанный на добавленных отладочных отпечатках:

  1. Загружен класс System/IComparable'1(ClassFromModuleFoo) mscorlib.
  2. Сразу после этого класс ClassFromModuleFoo модуля Foo загружается в сборку # 1.
  3. Завершается модуль Fooдля загрузки в сборку # 1.
  4. Затем модуль Foo снова загружается в другую сборку, # 2.
  5. IComparable и ClassFromModuleFoo загружаются снова, на этот раз в сборку# 2.Теперь есть два экземпляра каждого класса: один в Foo, загруженный в сборку # 1, и один в Foo, загруженный в сборку # 2.
  6. Модуль Foo начинает выгружаться из сборки # 1.
  7. ClassUnloadStarted обратный вызов вызывается для ClassFromModuleFoo в сборке # 1.
  8. Модуль Foo завершен для выгрузки из сборки # 1.
  9. ClassUnloadStarted is not , требуемый дляSystem/IComparable'1(ClassFromModuleFoo) сборки # 1 в любое время позже (даже если его модуль выгружен и его ClassID указывает на теперь перегруженную память).

Некоторая дополнительная информация:

  • Проблема такжевоспроизводится с последней версией .NET Framework, предварительный просмотр 4.8.
  • Я отключил собственные изображения, добавив COR_PRF_DISABLE_ALL_NGEN_IMAGES в маску событий профилировщика, полагая, что это может повлиять на обратные вызовы ClassLoad *, но это не помоглолюбая разница.Я проверил, что mscorlib.dll действительно загружено вместо его собственного изображения.

Редактировать:

Благодаря моему очень умному коллеге, я смог воспроизвестипроблема с небольшим примером проекта, который имитирует этот сценарий путем загрузки и выгрузки доменов приложений.Вот оно:
https://github.com/shaharv/dotnet/tree/master/testers/module-load-unload

Сбой происходит для этого класса в тесте, который выгружен и для которого CLR не вызвал обратный вызов выгрузки:

Loop/MyGenList`1<System/String>

Вот соответствующий код, который загружается и выгружается несколько раз:

namespace Loop
{
    public class MyGenList<T>
    {
        public List<T> _tList;

        public MyGenList(List<T> tList)
        {
            _tList = tList;
        }
    }

    class MyGenericTest
    {
        public void TestFunc()
        {
            MyGenList<String> genList = new MyGenList<String>(new List<string> { "A", "B", "C" });

            try
            {
                throw new Exception();
            }
            catch (Exception)
            {

            }
        }
    }
}

В какой-то момент профилировщик падает, пытаясь запросить ClassID этого класса - думая, что он все еще действителен, поскольку обратный вызов unloadне был вызван для этого.

Кстати, я попытался перенести этот пример на .NET Core для дальнейшего изучения, но не смог понять, как, поскольку .NET Core не поддерживает вторичные домены приложений (иЯ не совсем уверен, что он поддерживает разгрузку сборок по требованию в целом).

1 Ответ

0 голосов
/ 13 августа 2019

Сделав это возможным в .Net Core (выгрузка не поддерживалась до 3.0), нам удалось воспроизвести его (спасибо valiano!).Команда coreclr подтверждает, что это ошибка (https://github.com/dotnet/coreclr/issues/26126).

Из объяснения Дэвмасона:

Здесь задействованы три отдельных типа, и каждый обратный вызов дает вам только два (нодругой набор из двух).

Plugin.MyGenList1: универсальный тип без привязки Plugin.MyGenList1: универсальный тип, связанный с каноническим типом (используется для обычных ссылок) Plugin.MyGenList1: универсальный тип, связанный с System.String.Для ClassLoadStarted у нас есть логика, которая специально исключает несвязанные универсальные типы (т.е. Plugin.MyGenList1) из показа профилировщику в ClassLoader :: Notify

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

Проблема заключается в том, что мы применяем другой набор фильтрации для ClassUnloadStarted.Этот обратный вызов происходит внутри EEClass :: Destruct,и Destruct вызывается только для неуниверсальных типов, несвязанных универсальных типов и канонических универсальных типов.Неканонические универсальные типы (например, Plugin.MyGenList1) пропускаются.

...