Wyciek GDI (lub po prostu użycie zbyt wielu obiektów GDI) to jeden z najczęstszych problemów. W końcu powoduje problemy z renderowaniem, błędy i/lub problemy z wydajnością. Artykuł opisuje, jak debugujemy ten problem.
W 2016 roku, kiedy większość programów jest wykonywana w piaskownicach, z których nawet najbardziej niekompetentny programista nie może zaszkodzić systemowi, ze zdumieniem mierzę się z problemem, o którym będę mówił w tym artykule. Szczerze mówiąc miałem nadzieję, że wraz z Win32Api ten problem zniknął na zawsze. Niemniej jednak stawiłem czoła temu. Wcześniej słyszałem tylko przerażające historie o tym od starszych, bardziej doświadczonych programistów.
Problem
Wyciek lub użycie ogromnej ilości obiektów GDI.
Objawy
- Kolumna obiektów GDI na karcie Szczegóły Menedżera zadań pokazuje krytyczne 10000 (jeśli ta kolumna jest nieobecna, możesz ją dodać, klikając prawym przyciskiem myszy nagłówek tabeli i wybierając opcję Wybierz kolumny).
- Podczas programowania w C# lub w innych językach, które są wykonywane przez CLR, pojawia się następujący błąd zawierający mało informacji:
Wiadomość:Wystąpił błąd ogólny w GDI+.
Źródło:System.Drawing
TargetSite:IntPtr GetHbitmap(System.Drawing.Color)
Typ:System.Runtime.InteropServices.ExternalException
Błąd może nie wystąpić przy niektórych ustawieniach lub w niektórych wersjach systemu, ale Twoja aplikacja nie będzie w stanie wyrenderować pojedynczego obiektu: - Podczas programowania w С/С++, wszystkie metody GDI, takie jak Create%SOME_GDI_OBJECT%, zaczęły zwracać NULL.
Dlaczego?
Systemy Windows nie pozwalają na tworzenie więcej niż 65535 Obiekty GDI. Ta liczba jest rzeczywiście imponująca i trudno mi sobie wyobrazić normalny scenariusz wymagający tak ogromnej ilości obiektów. Istnieje ograniczenie dla procesów – 10000 na proces, który można zmodyfikować (poprzez zmianę HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota wartość z zakresu od 256 do 65535), ale firma Microsoft nie zaleca zwiększania tego ograniczenia. Jeśli nadal to zrobisz, jeden proces będzie w stanie zamrozić system tak, że nie będzie w stanie wyrenderować nawet komunikatu o błędzie. W takim przypadku system można przywrócić dopiero po ponownym uruchomieniu.
Jak naprawić?
Jeśli żyjesz w wygodnym i zarządzanym świecie CLR, istnieje duże prawdopodobieństwo, że w Twojej aplikacji występuje zwykły wyciek pamięci. Problem jest nieprzyjemny, ale to dość zwyczajny przypadek. Istnieje co najmniej tuzin świetnych narzędzi do wykrywania tego. Będziesz musiał użyć dowolnego profilera, aby zobaczyć, czy zwiększa się liczba obiektów, które otaczają zasoby GDI (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics). Jeśli tak, możesz przestać czytać ten artykuł. Jeśli wyciek opakowujących obiektów nie został wykryty, Twój kod korzysta bezpośrednio z interfejsu GDI API i istnieje scenariusz, w którym nie zostaną one usunięte
Co polecają inni?
Oficjalne wytyczne firmy Microsoft lub inne artykuły na ten temat zarekomendują ci coś takiego:
Znajdź wszystkie Utwórz %SOME_GDI_OBJECT% i sprawdź, czy odpowiedni DeleteObject (lub ReleaseDC dla obiektów HDC). Jeśli takie DeleteObject istnieje, może istnieć scenariusz, który tego nie nazywa.
Istnieje nieco ulepszona wersja tej metody, która zawiera dodatkowy krok:
Pobierz narzędzie GDIView. Może pokazywać dokładną liczbę obiektów GDI według typu. Zauważ, że całkowita liczba obiektów nie odpowiada wartości w ostatniej kolumnie. Ale możemy zamknąć na to oczy, jeśli pomoże to zawęzić pole wyszukiwania.
Projekt nad którym pracuję ma bazę kodu 9 milionów rekordów, mniej więcej tyle samo rekordów znajduje się w bibliotekach firm trzecich, setki wywołań funkcji GDI rozłożone na dziesiątki plików. Zmarnowałem dużo czasu i energii, zanim zrozumiałem, że ręczna analiza bez błędów jest niemożliwa.
Co mogę zaoferować?
Jeśli ta metoda wydaje ci się zbyt długa i męcząca, to nie przeszedłeś wszystkich etapów rozpaczy z poprzednim. Możesz spróbować wykonać poprzednie kroki, ale jeśli to nie pomoże, nie zapomnij o tym rozwiązaniu.
W pogoni za wyciekiem zadałem sobie pytanie:Gdzie są tworzone wyciekające obiekty? Nie można było ustawić punktów przerwania we wszystkich miejscach, w których wywoływana jest funkcja API. Poza tym nie byłem pewien, czy nie dzieje się to w .NET Framework lub w jednej z bibliotek firm trzecich, z których korzystamy. Kilka minut googlowania doprowadziło mnie do narzędzia API Monitor, które pozwalało rejestrować i śledzić wywołania wszystkich funkcji systemu. Z łatwością znalazłem listę wszystkich funkcji generujących obiekty GDI, zlokalizowałem i zaznaczyłem je w API Monitor. Następnie ustawiam punkty przerwania.
Następnie przeprowadziłem proces debugowania w Visual Studio i zaznaczono go w drzewie procesów. Piąty punkt przerwania zadziałał natychmiast:
Zdałem sobie sprawę, że utopię się w tym potoku i że potrzebuję czegoś innego. Usunąłem breakpointy z funkcji i postanowiłem wyświetlić dziennik. Pokazał tysiące połączeń. Stało się jasne, że nie będę w stanie przeanalizować ich ręcznie.
Zadanie polega na Znalezieniu wywołań funkcji GDI, które nie powodują usunięcia . Dziennik zawierał wszystko, czego potrzebowałem:listę wywołań funkcji w porządku chronologicznym, ich zwracane wartości i parametry. Dlatego musiałem uzyskać zwróconą wartość funkcji Create%SOME_GDI_OBJECT% i znaleźć wywołanie DeleteObject z tą wartością jako argumentem. Wybrałem wszystkie rekordy w API Monitor, wstawiłem je do pliku tekstowego i otrzymałem coś w rodzaju CSV z ogranicznikiem TAB. Uruchomiłem VS, w którym zamierzałem napisać mały program do parsowania, ale zanim mógł się załadować, przyszedł mi do głowy lepszy pomysł:wyeksportować dane do bazy danych i napisać zapytanie, aby znaleźć to, czego potrzebuję. To był właściwy wybór, ponieważ pozwolił mi szybko zadawać pytania i uzyskiwać odpowiedzi.
Istnieje wiele narzędzi do importowania danych z CSV do bazy danych, więc nie będę się rozwodził nad tym tematem (mysql, mssql, sqlite).
Mam następującą tabelę:
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 )
Napisałem następującą funkcję MySQL, aby uzyskać deskryptor usuniętego obiektu z wywołania API:
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
I na koniec napisałem zapytanie o lokalizację wszystkich obecnych obiektów:
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%';
(Zasadniczo po prostu znajdzie wszystkie połączenia Usuń dla wszystkich połączeń Utwórz).
Jak widać na powyższym obrazku, wszystkie połączenia bez jednego usunięcia zostały znalezione jednocześnie.
Pozostało więc ostatnie pytanie:Jak określić, skąd te metody są wywoływane w kontekście mojego kodu? I tu pomogła mi jedna wymyślna sztuczka:
- Uruchom aplikację w VS do debugowania
- Znajdź go w Api Monitor i wybierz.
- Wybierz wymaganą funkcję w API i umieść punkt przerwania.
- Naciskaj przycisk „Dalej”, aż zostanie wywołany z danymi parametrami (naprawdę przegapiłem warunkowe punkty przerwania z VS)
- Kiedy dojdziesz do wymaganego połączenia, przełącz się na CS i kliknij Przerwij wszystko .
- VS Debugger zostanie zatrzymany w miejscu, w którym tworzony jest wyciekający obiekt i jedyne, co musisz zrobić, to dowiedzieć się, dlaczego nie został usunięty.
Uwaga:kod został napisany w celach ilustracyjnych.
Podsumowanie:
Opisany algorytm jest skomplikowany i wymaga wielu narzędzi, ale dał wynik znacznie szybciej w porównaniu z głupim wyszukiwaniem w ogromnej bazie kodu.
Oto podsumowanie wszystkich kroków:
- Wyszukaj wycieki pamięci obiektów opakowujących GDI.
- Jeśli istnieją, usuń je i powtórz krok 1.
- Jeśli nie ma wycieków, wyszukaj jawnie wywołania funkcji API.
- Jeśli ich ilość nie jest duża, wyszukaj skrypt, w którym obiekt nie zostanie usunięty.
- Jeśli ich ilość jest duża lub trudno je wyśledzić, pobierz Monitor API i skonfiguruj go do rejestrowania wywołań funkcji GDI.
- Uruchom aplikację do debugowania w VS.
- Odtwórz wyciek (zainicjuje program w celu ukrycia spieniężonych obiektów).
- Połącz z Monitorem API.
- Odtwórz wyciek.
- Skopiuj dziennik do pliku tekstowego, zaimportuj go do dowolnej dostępnej bazy danych (skrypty opisane w tym artykule dotyczą MySQL, ale można je łatwo dostosować do dowolnego systemu zarządzania relacyjnymi bazami danych).
- Porównaj metody Create i Delete (skrypt SQL znajdziesz w tym artykule powyżej) i znajdź metody bez wywołań Delete.
- Ustaw punkt przerwania w Monitorze API przy wywołaniu wymaganej metody.
- Naciskaj przycisk Kontynuuj, aż metoda zostanie wywołana z ponownie uzyskanymi parametrami.
- Kiedy metoda zostanie wywołana z wymaganymi parametrami, kliknij Przerwij wszystko w VS.
- Dowiedz się, dlaczego ten obiekt nie został usunięty.
Mam nadzieję, że ten artykuł będzie przydatny i pomoże Ci zaoszczędzić czas.