GDI-lek (of gewoon het gebruik van te veel GDI-objecten) is een van de meest voorkomende problemen. Het veroorzaakt uiteindelijk weergaveproblemen, fouten en/of prestatieproblemen. Het artikel beschrijft hoe we dit probleem oplossen.
In 2016, wanneer de meeste programma's worden uitgevoerd in sandboxen waarvan zelfs de meest incompetente ontwikkelaar het systeem niet kan schaden, sta ik versteld van het probleem dat ik in dit artikel zal bespreken. Eerlijk gezegd hoopte ik dat dit probleem voor altijd was verdwenen samen met Win32Api. Toch liep ik er tegenaan. Daarvoor hoorde ik er alleen maar horrorverhalen over van oude, meer ervaren ontwikkelaars.
Het probleem
Lekkage of gebruik van de enorme hoeveelheid GDI-objecten.
Symptomen
- De kolom GDI-objecten op het tabblad Details van Taakbeheer toont kritieke 10000 (als deze kolom afwezig is, kunt u deze toevoegen door met de rechtermuisknop op de tabelkop te klikken en Kolommen selecteren te selecteren).
- Bij het ontwikkelen in C# of in andere talen die worden uitgevoerd door CLR, treedt de volgende slecht informatieve fout op:
Bericht:Er is een algemene fout opgetreden in GDI+.
Bron:System.Drawing
Doelsite:IntPtr GetHbitmap(System.Drawing.Color)
Type:System.Runtime.InteropServices.ExternalException
De fout treedt mogelijk niet op bij bepaalde instellingen of in bepaalde systeemversies, maar uw toepassing kan geen enkel object weergeven: - Tijdens de ontwikkeling in С/С++ begonnen alle GDI-methoden, zoals Create%SOME_GDI_OBJECT%, NULL terug te geven.
Waarom?
Op Windows-systemen is het niet toegestaan om meer dan 65535 . te maken GDI-objecten. Dit aantal is in feite indrukwekkend en ik kan me nauwelijks een normaal scenario voorstellen dat zo'n enorme hoeveelheid objecten vereist. Er is een beperking voor processen - 10000 per proces die kunnen worden gewijzigd (door de HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota te wijzigen waarde in het bereik van 256 tot 65535), maar Microsoft raadt af deze beperking te verhogen. Als u het nog steeds doet, kan één proces het systeem bevriezen, zodat het zelfs de foutmelding niet kan weergeven. In dit geval kan het systeem pas weer tot leven worden gewekt nadat het opnieuw is opgestart.
Hoe op te lossen?
Als u in een comfortabele en beheerde CLR-wereld leeft, is de kans groot dat u een gebruikelijk geheugenlek in uw toepassing heeft. Het probleem is onaangenaam, maar het is een vrij gewoon geval. Er zijn minstens een dozijn geweldige tools om dit te detecteren. U moet een profiler gebruiken om te zien of het aantal objecten dat GDI-bronnen omvat (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics) toeneemt. Als dit het geval is, kunt u stoppen met het lezen van dit artikel. Als het lekken van wrapper-objecten niet is gedetecteerd, gebruikt uw code rechtstreeks de GDI-API en is er een scenario waarin ze niet worden verwijderd
Wat raden anderen aan?
De officiële Microsoft-richtlijnen of andere artikelen over dit onderwerp zullen u zoiets als dit aanbevelen:
Vind alle Maken %SOME_GDI_OBJECT% en detecteren of het corresponderende DeleteObject (of ReleaseDC voor HDC-objecten) bestaat. Als zo'n DeleteObject bestaat, kan er een scenario zijn dat het niet noemt.
Er is een licht verbeterde versie van deze methode die een extra stap bevat:
Download het hulpprogramma GDIView. Het kan het exacte aantal GDI-objecten per type weergeven. Merk op dat het totale aantal objecten niet overeenkomt met de waarde in de laatste kolom. Maar we kunnen de ogen sluiten als het helpt om het zoekveld te verfijnen.
Het project waar ik aan werk heeft de codebasis van 9 miljoen records, ongeveer hetzelfde aantal records bevindt zich in de bibliotheken van derden, honderden oproepen van de GDI-functie die verspreid zijn over tientallen bestanden. Ik had veel tijd en energie verspild voordat ik begreep dat handmatige analyse zonder fouten onmogelijk is.
Wat kan ik aanbieden?
Als deze methode je te lang en vermoeiend lijkt, heb je met de vorige niet alle stadia van wanhoop gepasseerd. U kunt proberen de vorige stappen te volgen, maar als het niet helpt, vergeet deze oplossing dan niet.
Bij het nastreven van het lek vroeg ik mezelf af:Waar worden de lekkende objecten gemaakt? Het was onmogelijk om breekpunten in te stellen op alle plaatsen waar de API-functie wordt aangeroepen. Bovendien was ik er niet zeker van dat het niet gebeurt in het .NET Framework of in een van de externe bibliotheken die we gebruiken. Een paar minuten googlen leidde me naar het hulpprogramma API Monitor waarmee ik oproepen naar alle systeemfuncties kon loggen en traceren. Ik heb gemakkelijk de lijst gevonden met alle functies die GDI-objecten genereren, gelokaliseerd en geselecteerd in API Monitor. Vervolgens stel ik breekpunten in.
Daarna heb ik het foutopsporingsproces uitgevoerd in Visual Studio en selecteerde het in de Processen-structuur. Het vijfde breekpunt is meteen gelukt:
Ik realiseerde me dat ik zou verdrinken in deze stortvloed en dat ik iets anders nodig had. Ik heb onderbrekingspunten uit functies verwijderd en besloot het logboek te bekijken. Het toonde duizenden oproepen. Het werd duidelijk dat ik ze niet handmatig kan analyseren.
De taak is om de aanroepen van de GDI-functies te vinden die niet de verwijdering veroorzaken . Het logboek bevatte alles wat ik nodig had:de lijst met functieaanroepen in chronologische volgorde, hun geretourneerde waarden en parameters. Daarom moest ik een geretourneerde waarde van de functie Create%SOME_GDI_OBJECT% krijgen en de aanroep van DeleteObject met deze waarde als argument vinden. Ik selecteerde alle records in API Monitor, plaatste ze in een tekstbestand en kreeg zoiets als CSV met het TAB-scheidingsteken. Ik draaide VS, waar ik van plan was een klein programma te schrijven om te ontleden, maar voordat het kon laden, kwam er een beter idee in me op:gegevens exporteren naar een database en een query schrijven om te vinden wat ik nodig heb. Het was de juiste keuze omdat ik hierdoor snel vragen kon stellen en antwoorden kon krijgen.
Er zijn veel tools om gegevens van CSV naar een database te importeren, dus ik zal niet ingaan op dit onderwerp (mysql, mssql, sqlite).
Ik heb de volgende 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 )
Ik heb de volgende MySQL-functie geschreven om de descriptor van het verwijderde object uit de API-aanroep te halen:
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
En tot slot schreef ik een vraag om alle huidige objecten te lokaliseren:
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%';
(Kortom, het vindt gewoon alle Verwijder-oproepen voor alle Maak-oproepen).
Zoals je op de afbeelding hierboven kunt zien, zijn alle oproepen zonder een enkele verwijdering in één keer gevonden.
Dus de laatste vraag is overgebleven:hoe te bepalen, waar worden deze methoden in de context van mijn code genoemd? En hier heeft een mooie truc me geholpen:
- Voer de toepassing in VS uit voor foutopsporing
- Zoek het in Api Monitor en selecteer het.
- Selecteer een vereiste functie in API en plaats een breekpunt.
- Blijf op 'Volgende' klikken totdat het wordt aangeroepen met de betreffende parameters (ik heb echt voorwaardelijke breekpunten van VS gemist)
- Als u bij het gewenste gesprek komt, schakelt u over naar CS en klikt u op Alles verbreken .
- VS Debugger wordt gestopt waar het lekkende object is gemaakt en het enige wat u hoeft te doen is uitzoeken waarom het niet wordt verwijderd.
Opmerking:de code is ter illustratie geschreven.
Samenvatting:
Het beschreven algoritme is ingewikkeld en vereist veel tools, maar het gaf het resultaat veel sneller in vergelijking met een domme zoektocht door de enorme codebasis.
Hier is een samenvatting van alle stappen:
- Zoeken naar geheugenlekken van GDI-wrapperobjecten.
- Als ze bestaan, verwijder ze dan en herhaal stap 1.
- Als er geen lekken zijn, zoek dan expliciet naar aanroepen naar de API-functies.
- Als hun aantal niet groot is, zoek dan naar een script waarin een object niet wordt verwijderd.
- Als hun aantal groot is of nauwelijks kan worden getraceerd, download dan API Monitor en stel het in voor het loggen van aanroepen van de GDI-functies.
- Voer de toepassing voor foutopsporing uit in VS.
- Reproduceer het lek (het zal het programma initialiseren om de verzilverde objecten te verbergen).
- Maak verbinding met API Monitor.
- Reproduceer het lek.
- Kopieer het logboek naar een tekstbestand, importeer het in een willekeurige database (de scripts in dit artikel zijn voor MySQL, maar ze kunnen gemakkelijk worden gebruikt voor elk relationeel databasebeheersysteem).
- Vergelijk de Create- en Delete-methoden (u kunt het SQL-script vinden in dit artikel hierboven) en zoek de methoden zonder de Delete-aanroepen.
- Stel een onderbrekingspunt in API Monitor in op de aanroep van de vereiste methode.
- Blijf op Doorgaan klikken totdat de methode wordt aangeroepen met opnieuw verworven parameters.
- Als de methode wordt aangeroepen met de vereiste parameters, klikt u op Alles breken in VS.
- Ontdek waarom dit object niet wordt verwijderd.
Ik hoop dat dit artikel nuttig zal zijn en u zal helpen tijd te besparen.