Строго говоря, это оригинальный текст статьи, а в блог уже перевел.
Здесь статья публикуется чуть позже и только благодаря этому получает тег перевода.
В 2016 году, когда большинство программ выполняются в песочницах, из которых даже самый некомпетентный разработчик не сможет нанести вред системе, странно столкнуться с проблемой, о которой пойдет речь ниже.
Честно говоря, я надеялся, что он ушёл в далекое прошлое вместе с Win32Api, но недавно наткнулся на него.
До этого я слышал только ужасные истории от старших, более опытных разработчиков о том, что такое может случиться.
Проблема
Утечка или использование слишком большого количества объектов GDI.Симптомы:
В диспетчере задач на вкладке «Сведения» в столбце «Объекты GDI» отображается тревожная цифра 10000 (если этого столбца нет, его можно добавить, щелкнув правой кнопкой мыши заголовок таблицы и выбрав «Выбрать столбцы»).При разработке на C# или другом языке, выполняемом CLR, будет выдано неспецифическое исключение:
Сообщение: В GDI+ произошла общая ошибка.Также при определенных настройках или версии системы исключения может и не быть, но ваше приложение не сможет отрисовать ни одного объекта.Источник: Система.
Рисование TargetSite: IntPtr GetHbitmap(System.Drawing.Color) Тип: System.Runtime.InteropServices.ExternalException
При разработке на C/C++ все методы GDI, такие как Create%SOME_GDI_OBJECT%, начали возвращать NULL.
Почему?
В системах семейства Windows одновременно можно создать максимум 65 535 объектов GDI. На самом деле эта цифра невероятно велика и не должна приближаться к достигнутой при любом нормальном сценарии.У процесса есть ограничение в 10 000, которое хоть и можно изменить (в реестре изменить значение HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota в диапазоне от 256 до 65535), но Microsoft настоятельно не рекомендует увеличивать этот лимит. Если это сделать, то у одного процесса появится возможность настолько сильно обрушить систему, что она даже не сможет нарисовать сообщение об ошибке.
В этом случае система сможет ожить только после перезагрузки.
Как это исправить?
Если вы живете в аккуратном мире, управляемом CLR, то вероятность того, что в вашем приложении есть простая утечка памяти, составляет 9 из 10. Проблема хоть и неприятная, но довольно распространенная и для ее поиска существует не менее десятка отличных инструментов.Я не буду подробно останавливаться на этом.
Вам просто нужно использовать любой профилировщик, чтобы увидеть, увеличивается ли количество объектов-оберток над ресурсами GDI: Кисть, Растровое изображение, Перо, Регион, Графика.
Если это действительно так, то вам повезло, вы можете закрыть вкладку со статьей.
Если не обнаружены просочившиеся объекты-оболочки, то в вашем коде напрямую используются функции GDI и сценарий, в котором они не удаляются.
Что вам посоветуют другие?
Официальное руководство от Microsoft или другие статьи по этому поводу, которые вы найдете в Интернете, посоветуют примерно следующее: Найти все Создавать %SOME_GDI_OBJECT% и узнайте, есть ли соответствующий УдалитьОбъект (или ReleaseDC для объектов HDC), а если есть, то, возможно, существует сценарий, при котором он не будет вызываться.Существует немного улучшенная версия этого метода, она содержит дополнительный первый шаг: Загрузите утилиту GDIView .
Он может показывать определенное количество объектов GDI по типам, и единственное, что настораживает, это то, что сумма всех не соответствует значению в последнем столбце.
Вы можете попробовать игнорировать это, если это поможет как-то сузить область поиска.
Кодовая база проекта, над которым я работаю, превышает 9 миллионов строк и примерно столько же в сторонних библиотеках, сотни вызовов функций GDI, разбросанных по десяткам файлов.
Я потратил много сил и кофе, прежде чем понял, что проанализировать это вручную и ничего не упустить просто невозможно.
Что я предложу?
Если этот метод кажется вам слишком длинным и требует лишних телодвижений, значит, вы еще не прошли все стадии отчаяния с предыдущим.Вы можете попробовать предыдущие действия еще несколько раз, но если это не поможет, то не сбрасывайте со счетов этот вариант. В поисках утечки я спросил себя: «Где находятся объекты, создавшие утечкуЭ» Было абсолютно невозможно установить точки останова во всех местах вызова API-функций.
Кроме того, мы не были до конца уверены, что этого не происходит в .
net framework или одной из сторонних библиотек, которые мы используем.
Несколько минут гугления привели меня к утилите API-монитор , что позволило логировать и отлаживать вызовы любых системных функций.
Я легко нашел список всех функций, генерирующих GDI-объекты, честно нашел их и выбрал в Api Monitor, а затем установил точки останова.
После которого Я начал процесс отладки в Visual Studio. , и вот я выбрал его в дереве процессов.
Первая точка останова сработала сразу:
Было слишком много проблем.
Я быстро понял, что утону в этом потоке и нужно придумать что-то еще.
Я удалил точки останова из функций и решил посмотреть лог.
Звонков были тысячи и тысячи.
Стало очевидным, что их невозможно проанализировать вручную.
Задача: Найдите те вызовы функций GDI, которые не были удалены.
В журнале содержится все необходимое: список вызовов функций в хронологическом порядке, их возвращаемые значения и параметры.
Оказывается, мне нужно взять возвращаемое значение функции Create%SOME_GDI_OBJECT% и найти вызов DeleteObject с этим значением в качестве аргумента.
Я выделил все записи в Api Monitor, вставил их в текстовый файл и получил что-то вроде CSV с разделителями TAB. Я запустил VS, где подумывал написать программу для парсинга этого, но прежде чем она загрузилась, мне пришла в голову идея получше: экспортировать данные в базу данных и написать запрос, чтобы выковырять то, что меня интересует. Это был правильный выбор, потому что он позволил мне очень быстро задавать вопросы и получать ответы.
Существует множество инструментов для импорта данных из CSV в базу данных, поэтому я не буду на этом останавливаться ( MySQL , MSSQL , Склайт ).
У меня есть такая таблица:
Я написал функцию MySQL, чтобы получить дескриптор удаляемого объекта из вызова API:-- mysql code CREATE TABLE apicalls ( id int(11) DEFAULT NULL, `Time of Day` datetime DEFAULT NULL, Thread int(11) DEFAULT NULL, Module varchar(50) DEFAULT NULL, API varchar(200) DEFAULT NULL, `Return Value` varchar(50) DEFAULT NULL, Error varchar(100) DEFAULT NULL, Duration varchar(50) DEFAULT NULL )
CREATE FUNCTION getHandle(api varchar(1000))
RETURNS varchar(100) CHARSET utf8
BEGIN
DECLARE start int(11);
DECLARE result varchar(100);
SET start := INSTR(api,','); -- for ReleaseDC where HDC is second parameter. ex: 'ReleaseDC ( 0x0000000000010010, 0xffffffffd0010edf )'
IF start = 0 THEN
SET start := INSTR(api, '(');
END IF;
SET result := SUBSTRING_INDEX(SUBSTR(api, start + 1), ')', 1);
RETURN TRIM(result);
END
И, наконец, запрос, который найдет все текущие объекты:
SELECT creates.id, creates.handle chandle, creates.API, dels.API deletedApi
FROM (SELECT a.id, a.`Return Value` handle, a.API FROM apicalls a WHERE a.API LIKE 'Create%') creates
LEFT JOIN (SELECT
d.id,
d.API,
getHandle(d.API) handle
FROM apicalls d
WHERE API LIKE 'DeleteObject%'
OR API LIKE 'ReleaseDC%' LIMIT 0, 100) dels
ON dels.handle = creates.handle
WHERE creates.API LIKE 'Create%';
(Строго говоря, он будет просто сопоставлять все вызовы удаления со всеми вызовами создания)
На рисунке сразу показаны вызовы, для которых не найдено ни одного удаления.
Остался последний вопрос: Как я могу найти, откуда вызываются эти методы в контексте моего кода? И тут мне помог один хитрый трюк: Запустите приложение для отладки в VS. Найдите его в Api Monitor и выберите.
Выберите нужную функцию API и установите точку останова.
Терпеливо нажимайте «Далее», пока не появится окно с интересующими вас параметрами.
(Как я пропустил условные точки останова из vs Когда дойдете до нужного звонка, зайдите в VS и нажмите «Разбить все».
Отладчик VS остановится в момент создания объекта утечки и останется только выяснить, почему он не удаляется.
(Код написан только для примера)
Краткое содержание:
Алгоритм длинный и сложный, использует множество инструментов, но он дал мне результаты гораздо быстрее, чем тупой поиск ошибок в огромной базе кода.Вот оно, для тех, кому было лень читать или кто уже забыл, с чего все начиналось, пока читал: Ищите утечки памяти в объектах-оболочках GDI. Если таковые имеются, устраните их и повторите первый шаг.
Если их нет, то ищите вызовы функций API напрямую.
Если их мало, то ищите сценарий, при котором объект может быть не удален.
Если их много или вы не можете их отследить, то вам необходимо скачать Api Monitor и настроить его для логирования вызовов функций GDI. Запустите приложение для отладки в VS. Воспроизведите утечку (это инициализирует программу, чтобы кэшированные объекты не появлялись в журнале как бельмо на глазу).
Подключитесь к Api Monitor. Воспроизведите утечку.
Скопируйте лог в текстовый файл, импортируйте в любую базу данных, которая есть под рукой (скрипты в статье предназначены для mysql, но легко адаптируются под любую СУБД) Сравните методы Create и Delete (SQL-скрипт выше в этой статье), найдите те, которые не вызывают Delete Установите точку останова в Api Monitor для вызова нужного метода.
Нажимайте продолжить до тех пор, пока не будет вызван метод с необходимыми параметрами.
Плачьте об отсутствии условных точек останова.
Когда метод будет вызван с необходимыми параметрами, нажмите «Разорвать все в VS».
Найдите, почему этот объект не удаляется.
Я очень надеюсь, что эта статья сэкономит кому-то много времени и окажется полезной.
Теги: #.
NET #C++ #winforms #windows form #GDI+ #native #неуправляемый код #.
NET #Системное программирование #отладка #разработка для Windows
-
Психология Восприятия Формы В Логотипах
19 Oct, 24 -
Взгляд С Другой Стороны
19 Oct, 24 -
Джанго: Не Изобретая Велосипед
19 Oct, 24 -
Хабролинч!
19 Oct, 24