sql >> Database teknologi >  >> RDS >> Database

Håndtering af en GDI-ressourcelækage

GDI-lækage (eller blot brugen af ​​for mange GDI-objekter) er et af de mest almindelige problemer. Det forårsager i sidste ende gengivelsesproblemer, fejl og/eller ydeevneproblemer. Artiklen beskriver, hvordan vi fejlretter dette problem.

I 2016, hvor de fleste programmer køres i sandkasser, hvorfra selv den mest inkompetente udvikler ikke kan skade systemet, er jeg overrasket over at stå over for det problem, jeg vil tale om i denne artikel. Helt ærligt håbede jeg, at dette problem var gået for evigt sammen med Win32Api. Ikke desto mindre stod jeg over for det. Før det hørte jeg lige rædselshistorier om det fra gamle mere erfarne udviklere.

Problemet

Lækage eller brug af den enorme mængde af GDI-objekter.

Symptomer

  1. GDI-objektkolonnen på fanen Detaljer i Task Manager viser kritiske 10000 (hvis denne kolonne er fraværende, kan du tilføje den ved at højreklikke på tabeloverskriften og vælge Vælg kolonner).
  2. Når der udvikles i C# eller på andre sprog, der udføres af CLR, opstår følgende dårligt informative fejl:
    Meddelelse:Der opstod en generisk fejl i GDI+.
    Kilde:System.Drawing
    TargetSite:IntPtr GetHbitmap(System.Drawing.Color)
    Type:System.Runtime.InteropServices.ExternalException
    Fejlen opstår muligvis ikke med visse indstillinger eller i visse systemversioner, men din applikation vil ikke være i stand til at gengive et enkelt objekt:
  3. Under udviklingen i С/С++ begyndte alle GDI-metoder, såsom Create%SOME_GDI_OBJECT%, at returnere NULL.

Hvorfor?

Windows-systemer tillader ikke oprettelse af mere end 65535 GDI objekter. Dette tal er faktisk imponerende, og jeg kan næsten ikke forestille mig et normalt scenarie, der kræver en så stor mængde genstande. Der er en begrænsning for processer – 10000 pr. proces, der kan ændres (ved at ændre HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota værdi i intervallet 256 til 65535), men Microsoft anbefaler ikke at øge denne begrænsning. Hvis du stadig gør det, vil en proces være i stand til at fryse systemet, så det ikke vil være i stand til at gengive selv fejlmeddelelsen. I dette tilfælde kan systemet kun genoplives efter genstart.

Hvordan løses problemet?

Hvis du lever i en komfortabel og administreret CLR-verden, er der stor chance for, at du har en sædvanlig hukommelseslækage i din applikation. Problemet er ubehageligt, men det er en ganske almindelig sag. Der er mindst et dusin af gode værktøjer til at opdage dette. Du skal bruge en hvilken som helst profiler for at se, om antallet af objekter, der omslutter GDI-ressourcer (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics) stiger. Hvis det er tilfældet, kan du stoppe med at læse denne artikel. Hvis lækagen af ​​wrapper-objekter ikke blev opdaget, bruger din kode GDI API direkte, og der er et scenario, hvor de ikke slettes

Hvad anbefaler andre?

Den officielle Microsoft-vejledning eller andre artikler om dette emne vil anbefale dig noget som dette:

Find alle Opret %SOME_GDI_OBJECT% og opdage, om det tilsvarende DeleteObject (eller ReleaseDC for HDC-objekter) eksisterer. Hvis sådan DeleteObject eksisterer, kan der være et scenarie, der ikke kalder det.

Der er en lidt forbedret version af denne metode, der indeholder et ekstra trin:

Download GDIView-værktøjet. Det kan vise det nøjagtige antal GDI-objekter efter type. Bemærk, at det samlede antal objekter ikke svarer til værdien i sidste kolonne. Men det kan vi godt lukke øjnene for, hvis det er med til at indsnævre søgefeltet.

Projektet, jeg arbejder på, har kodebasen på 9 millioner poster, omtrent den samme mængde poster er placeret i tredjepartsbibliotekerne, hundredvis af opkald af GDI-funktionen, som er spredt over snesevis af filer. Jeg havde spildt masser af tid og energi, før jeg forstod, at manuel analyse uden fejl er umulig.

Hvad kan jeg tilbyde?

Hvis denne metode virker for lang og trættende for dig, har du ikke bestået alle stadier af fortvivlelse med den forrige. Du kan prøve at følge de foregående trin, men hvis det ikke hjælper, så glem ikke denne løsning.

I jagten på lækagen spurgte jeg mig selv:Hvor er de utætte genstande skabt? Det var umuligt at sætte breakpoints alle steder, hvor API-funktionen kaldes. Desuden var jeg ikke sikker på, at det ikke sker i .NET Framework eller i et af de tredjepartsbiblioteker, vi bruger. Få minutters google førte mig til API Monitor-værktøjet, der gjorde det muligt at logge og spore opkald til alle systemfunktioner. Jeg har nemt fundet listen over alle de funktioner, der genererer GDI-objekter, lokaliseret og valgt dem i API Monitor. Derefter sætter jeg pausepunkter.

Derefter kørte jeg fejlretningsprocessen i Visual Studio og valgte det i procestræet. Det femte brudpunkt er løst med det samme:

Jeg indså, at jeg ville drukne i denne torrent, og at jeg havde brug for noget andet. Jeg slettede brudpunkter fra funktioner og besluttede at se loggen. Det viste tusindvis af opkald. Det blev klart, at jeg ikke vil være i stand til at analysere dem manuelt.

Opgaven er at finde opkaldene til de GDI-funktioner, der ikke forårsager sletningen . Loggen indeholdt alt, hvad jeg havde brug for:listen over funktionskald i kronologisk rækkefølge, deres returnerede værdier og parametre. Derfor var jeg nødt til at få en returneret værdi af funktionen Create%SOME_GDI_OBJECT% og finde kaldet til DeleteObject med denne værdi som et argument. Jeg valgte alle poster i API Monitor, indsatte dem i en tekstfil og fik noget som CSV med TAB afgrænseren. Jeg kørte VS, hvor jeg havde til hensigt at skrive et lille program til parsing, men før det kunne indlæses, kom der en bedre idé til mig:at eksportere data til en database og at skrive en forespørgsel for at finde det, jeg har brug for. Det var det rigtige valg, da det gav mig mulighed for hurtigt at stille spørgsmål og få svar.

Der er mange værktøjer til at importere data fra CSV til en database, så jeg vil ikke dvæle ved dette emne (mysql, mssql, sqlite).

Jeg har følgende 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
)

Jeg skrev følgende MySQL-funktion for at hente beskrivelsen af ​​det slettede objekt fra API-kaldet:

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

Og til sidst skrev jeg en forespørgsel til at lokalisere alle de aktuelle objekter:

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

(Dybest set vil den blot finde alle Slet opkald for alle Opret opkald).

Som du kan se på billedet ovenfor, er alle opkald uden en enkelt sletning fundet på én gang.

Så det sidste spørgsmål er blevet tilbage:Hvordan bestemmer man, hvorfra kaldes disse metoder i forbindelse med min kode? Og her hjalp et fancy trick mig:

  1. Kør applikationen i VS til fejlretning
  2. Find den i Api Monitor, og vælg den.
  3. Vælg en påkrævet funktion i API og placer et breakpoint.
  4. Bliv ved med at klikke på 'Næste', indtil det bliver kaldt med de pågældende parametre (jeg savnede virkelig betingede brudpunkter fra VS)
  5. Når du kommer til det påkrævede opkald, skal du skifte til CS og klikke på Afbryd alle .
  6. VS Debugger vil blive stoppet lige der, hvor det lækkede objekt er oprettet, og alt du skal gøre er at finde ud af, hvorfor det ikke er slettet.

Bemærk:Koden er skrevet til illustrationsformål.

Oversigt:

Den beskrevne algoritme er kompliceret og kræver mange værktøjer, men den gav resultatet meget hurtigere sammenlignet med en dum søgning gennem den enorme kodebase.

Her er en oversigt over alle trinene:

  1. Søg efter hukommelseslækager af GDI-indpakningsobjekter.
  2. Hvis de findes, skal du fjerne dem og gentage trin 1.
  3. Hvis der ikke er nogen lækager, skal du søge eksplicit efter kald til API-funktionerne.
  4. Hvis deres mængde ikke er stor, søg efter et script, hvor et objekt ikke er slettet.
  5. Hvis deres mængde er stor, eller de næsten ikke kan spores, skal du downloade API Monitor og indstille den til at logge opkald af GDI-funktionerne.
  6. Kør applikationen til fejlretning i VS.
  7. Reproducer lækagen (det vil initialisere programmet for at skjule de indkasserede objekter).
  8. Opret forbindelse til API Monitor.
  9. Reproducer lækagen.
  10. Kopiér loggen ind i en tekstfil, importer den til enhver database ved hånden (scripterne i denne artikel er til MySQL, men de kan nemt bruges til ethvert relationelt databasestyringssystem).
  11. Sammenlign Opret og Slet metoder (du kan finde SQL-scriptet i denne artikel ovenfor), og find metoderne uden Slet-kaldene.
  12. Indstil et brudpunkt i API Monitor ved kald af den påkrævede metode.
  13. Bliv ved med at klikke på Fortsæt, indtil metoden kaldes med genindhentede parametre.
  14. Når metoden kaldes med de nødvendige parametre, skal du klikke på Break All in VS.
  15. Find ud af, hvorfor dette objekt ikke er slettet.

Jeg håber, at denne artikel vil være nyttig og hjælpe dig med at spare tid.


  1. Sådan kontrollerer du størrelsen af ​​alle tabeller i en database i MySQL

  2. MariaDB Server 10.0.33 nu tilgængelig

  3. Brug af Oracle JDeveloper 12c med Oracle Database, del 2

  4. Eksporter MySQL-database ved hjælp af PHP