sql >> Databasteknik >  >> RDS >> Database

Hantera en GDI-resursläcka

GDI-läcka (eller helt enkelt användningen av för många GDI-objekt) är ett av de vanligaste problemen. Det orsakar så småningom renderingsproblem, fel och/eller prestandaproblem. Artikeln beskriver hur vi felsöker det här problemet.

Under 2016, när de flesta program körs i sandlådor varifrån även den mest inkompetenta utvecklaren inte kan skada systemet, är jag förvånad över att möta problemet jag kommer att tala om i den här artikeln. Ärligt talat hoppades jag att det här problemet hade försvunnit för alltid tillsammans med Win32Api. Ändå mötte jag det. Innan dess hörde jag bara skräckhistorier om det från gamla mer erfarna utvecklare.

Problemet

Läckage eller användning av den enorma mängden GDI-objekt.

Symtom

  1. GDI-objektkolumnen på fliken Detaljer i Aktivitetshanteraren visar kritiska 10000 (om denna kolumn saknas kan du lägga till den genom att högerklicka på tabellrubriken och välja Välj kolumner).
  2. När man utvecklar i C# eller på andra språk som körs av CLR, uppstår följande dåligt informativa fel:
    Meddelande:Ett allmänt fel inträffade i GDI+.
    Källa:System.Drawing
    TargetSite:IntPtr GetHbitmap(System.Drawing.Color)
    Typ:System.Runtime.InteropServices.ExternalException
    Felet kanske inte uppstår med vissa inställningar eller i vissa systemversioner, men din applikation kommer inte att kunna rendera ett enda objekt:
  3. Under utvecklingen i С/С++ började alla GDI-metoder, som Create%SOME_GDI_OBJECT%, returnera NULL.

Varför?

Windows-system tillåter inte att skapa mer än 65535 GDI-objekt. Denna siffra är faktiskt imponerande och jag kan knappt föreställa mig ett normalt scenario som kräver en så enorm mängd föremål. Det finns en begränsning för processer – 10 000 per process som kan ändras (genom att ändra HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota värde inom intervallet 256 till 65535), men Microsoft rekommenderar inte att du ökar denna begränsning. Om du fortfarande gör det kommer en process att kunna frysa systemet så att det inte ens kan återge felmeddelandet. I det här fallet kan systemet återupplivas först efter omstart.

Hur fixar jag det?

Om du lever i en bekväm och hanterad CLR-värld är det stor chans att du har en vanlig minnesläcka i din applikation. Problemet är obehagligt, men det är ett ganska vanligt fall. Det finns minst ett dussin bra verktyg för att upptäcka detta. Du kommer att behöva använda valfri profilerare för att se om antalet objekt som omsluter GDI-resurser (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics) ökar. Om så är fallet kan du sluta läsa den här artikeln. Om läckan av wrapperobjekt inte upptäcktes använder din kod GDI API direkt och det finns ett scenario när de inte raderas

Vad rekommenderar andra?

Den officiella Microsoft-vägledningen eller andra artiklar om detta ämne kommer att rekommendera dig något i stil med detta:

Hitta alla Skapa %SOME_GDI_OBJECT% och upptäck om motsvarande DeleteObject (eller ReleaseDC för HDC-objekt) finns. Om sådant DeleteObject finns, kan det finnas ett scenario som inte kallar det.

Det finns en något förbättrad version av denna metod som innehåller ytterligare ett steg:

Ladda ner GDIView-verktyget. Den kan visa det exakta antalet GDI-objekt per typ. Observera att det totala antalet objekt inte motsvarar värdet i den sista kolumnen. Men vi kan blunda för detta om det hjälper till att begränsa sökfältet.

Projektet jag arbetar med har kodbasen på 9 miljoner poster, ungefär samma mängd poster finns i tredjepartsbiblioteken, hundratals anrop av GDI-funktionen som är spridda över dussintals filer. Jag hade slösat bort mycket tid och energi innan jag förstod att manuell analys utan fel är omöjlig.

Vad kan jag erbjuda?

Om den här metoden verkar för lång och tröttsam för dig, har du inte klarat alla stadier av förtvivlan med den föregående. Du kan försöka följa de föregående stegen, men om det inte hjälper, glöm inte den här lösningen.

I jakten på läckan frågade jag mig själv:Var skapas de läckande föremålen? Det var omöjligt att sätta brytpunkter på alla ställen där API-funktionen anropas. Dessutom var jag inte säker på att det inte händer i .NET Framework eller i något av de tredjepartsbibliotek som vi använder. Några minuters googling ledde mig till API Monitor-verktyget som gjorde det möjligt att logga och spåra anrop till alla systemfunktioner. Jag har lätt hittat listan över alla funktioner som genererar GDI-objekt, lokaliserat och valt dem i API Monitor. Sedan ställer jag in brytpunkter.

Efter det körde jag felsökningsprocessen i Visual Studio och valde den i processträdet. Den femte brytpunkten har fungerat direkt:

Jag insåg att jag skulle drunkna i den här torrenten och att jag behövde något annat. Jag tog bort brytpunkter från funktioner och bestämde mig för att se loggen. Den visade tusentals samtal. Det blev klart att jag inte kommer att kunna analysera dem manuellt.

Uppgiften är att Hitta anropen av GDI-funktionerna som inte orsakar raderingen . Loggen innehöll allt jag behövde:listan över funktionsanrop i kronologisk ordning, deras returnerade värden och parametrar. Därför behövde jag få ett returnerat värde för funktionen Create%SOME_GDI_OBJECT% och hitta anropet till DeleteObject med detta värde som argument. Jag valde alla poster i API Monitor, infogade dem i en textfil och fick något som CSV med TAB-avgränsaren. Jag körde VS, där jag tänkte skriva ett litet program för att analysera, men innan det kunde laddas fick jag en bättre idé:att exportera data till en databas och att skriva en fråga för att hitta det jag behöver. Det var rätt val eftersom det gjorde det möjligt för mig att snabbt ställa frågor och få svar.

Det finns många verktyg för att importera data från CSV till en databas, så jag ska inte uppehålla mig vid detta ämne (mysql, mssql, sqlite).

Jag har följande tabell:

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
)

Jag skrev följande MySQL-funktion för att hämta beskrivningen av det borttagna objektet från API-anropet:

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

Och slutligen skrev jag en fråga för att lokalisera alla aktuella objekt:

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%';

(I grund och botten hittar den helt enkelt alla Ta bort samtal för alla Skapa samtal).

Som du ser på bilden ovan har alla samtal utan en enda radering hittats på en gång.

Så den sista frågan har lämnats:Hur avgör man, varifrån anropas dessa metoder i samband med min kod? Och här hjälpte ett fint trick mig:

  1. Kör programmet i VS för felsökning
  2. Hitta den i Api Monitor och välj den.
  3. Välj en önskad funktion i API och placera en brytpunkt.
  4. Fortsätt att klicka på "Nästa" tills det kommer att anropas med parametrarna i fråga (jag saknade verkligen villkorliga brytpunkter från VS)
  5. När du kommer till det önskade samtalet byter du till CS och klickar på Break All .
  6. VS Debugger stoppas precis där det läckande objektet skapas och allt du behöver göra är att ta reda på varför det inte raderas.

Obs:Koden är skriven i illustrationssyfte.

Sammanfattning:

Den beskrivna algoritmen är komplicerad och kräver många verktyg, men den gav resultatet mycket snabbare jämfört med en dum sökning genom den enorma kodbasen.

Här är en sammanfattning av alla steg:

  1. Sök efter minnesläckor av GDI-omslagsobjekt.
  2. Om de finns, eliminera dem och upprepa steg 1.
  3. Om det inte finns några läckor, sök explicit efter anrop till API-funktionerna.
  4. Om deras kvantitet inte är stor, sök efter ett skript där ett objekt inte raderas.
  5. Om deras kvantitet är stor eller om de knappt går att spåra, ladda ner API Monitor och ställ in den för att logga anrop av GDI-funktionerna.
  6. Kör programmet för felsökning i VS.
  7. Reproducera läckan (det kommer att initiera programmet för att dölja de inkasserade objekten).
  8. Anslut till API Monitor.
  9. Återskapa läckan.
  10. Kopiera loggen till en textfil, importera den till valfri databas (skripten i den här artikeln är för MySQL, men de kan enkelt användas för alla relationsdatabashanteringssystem).
  11. Jämför metoderna Skapa och Ta bort (du hittar SQL-skriptet i den här artikeln ovan) och hitta metoderna utan Ta bort-anropen.
  12. Ställ in en brytpunkt i API Monitor för anropet av den nödvändiga metoden.
  13. Fortsätt att klicka på Fortsätt tills metoden anropas med återförvärvade parametrar.
  14. När metoden anropas med nödvändiga parametrar, klicka på Bryt alla i VS.
  15. Ta reda på varför detta objekt inte raderas.

Jag hoppas att den här artikeln kommer att vara användbar och hjälpa dig att spara tid.


  1. Bästa alternativet för att lagra användarnamn och lösenord i Android-appen

  2. Hur man konfigurerar Glassfish Server i Eclipse manuellt

  3. mysql motsvarande datatyper

  4. Hibernate och Multi-Tenant Database med hjälp av scheman i PostgreSQL