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

Gestione di una perdita di risorse GDI

La perdita di GDI (o semplicemente l'utilizzo di troppi oggetti GDI) è uno dei problemi più comuni. Alla fine causa problemi di rendering, errori e/o problemi di prestazioni. L'articolo descrive come eseguire il debug di questo problema.

Nel 2016, quando la maggior parte dei programmi viene eseguita in sandbox da cui anche lo sviluppatore più incompetente non può danneggiare il sistema, sono stupito di affrontare il problema di cui parlerò in questo articolo. Francamente, speravo che questo problema fosse andato per sempre insieme a Win32Api. Ciononostante, l'ho affrontato. Prima di allora, ho appena sentito storie dell'orrore a riguardo da vecchi sviluppatori più esperti.

Il problema

Perdita o utilizzo dell'enorme quantità di oggetti GDI.

Sintomi

  1. La colonna degli oggetti GDI nella scheda Dettagli di Task Manager mostra 10000 critici (se questa colonna è assente, puoi aggiungerla facendo clic con il pulsante destro del mouse sull'intestazione della tabella e selezionando Seleziona colonne).
  2. Durante lo sviluppo in C# o in altri linguaggi eseguiti da CLR, si verifica il seguente errore scarsamente informativo:
    Messaggio:si è verificato un errore generico in GDI+.
    Fonte:System.Drawing
    Sito di destinazione:IntPtr GetHbitmap(System.Drawing.Color)
    Tipo:System.Runtime.InteropServices.ExternalException
    L'errore potrebbe non verificarsi con determinate impostazioni o in determinate versioni di sistema, ma la tua applicazione non sarà in grado di eseguire il rendering di un singolo oggetto:
  3. Durante lo sviluppo in С/С++, tutti i metodi GDI, come Create%SOME_GDI_OBJECT%, hanno iniziato a restituire NULL.

Perché?

I sistemi Windows non consentono di creare più di 65535 oggetti GDI. Questo numero, infatti, è impressionante e difficilmente riesco a immaginare uno scenario normale che richieda una così grande quantità di oggetti. Esiste una limitazione per i processi:10000 per processo che possono essere modificati (modificando HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota valore compreso tra 256 e 65535), ma Microsoft sconsiglia di aumentare questa limitazione. Se lo fai ancora, un processo sarà in grado di bloccare il sistema in modo che non sia in grado di visualizzare nemmeno il messaggio di errore. In questo caso, il sistema può essere ripristinato solo dopo il riavvio.

Come risolvere?

Se vivi in ​​un mondo CLR comodo e gestito, c'è un'alta probabilità che tu abbia una normale perdita di memoria nella tua applicazione. Il problema è spiacevole, ma è un caso abbastanza normale. C'è almeno una dozzina di ottimi strumenti per rilevare questo. Sarà necessario utilizzare qualsiasi profiler per visualizzare se il numero di oggetti che racchiudono le risorse GDI (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics) aumenta. In tal caso, puoi interrompere la lettura di questo articolo. Se la perdita di oggetti wrapper non è stata rilevata, il tuo codice utilizza direttamente l'API GDI e si verifica uno scenario in cui non vengono eliminati

Cosa consigliano gli altri?

La guida ufficiale di Microsoft o altri articoli su questo argomento ti consiglieranno qualcosa del genere:

Trova tutti i Crea %SOME_GDI_OBJECT% e rilevare se il corrispondente DeleteObject (o ReleaseDC per oggetti HDC) esiste. Se tale DeleteObject esiste, potrebbe esserci uno scenario che non lo chiama.

Esiste una versione leggermente migliorata di questo metodo che contiene un passaggio aggiuntivo:

Scarica l'utilità GDIView. Può mostrare il numero esatto di oggetti GDI per tipo. Si noti che il numero totale di oggetti non corrisponde al valore nell'ultima colonna. Ma possiamo chiudere gli occhi su questo se aiuta a restringere il campo di ricerca.

Il progetto su cui sto lavorando ha una base di codice di 9 milioni di record, circa la stessa quantità di record si trova nelle librerie di terze parti, centinaia di chiamate della funzione GDI che sono distribuite su decine di file. Avevo perso molto tempo ed energie prima di capire che l'analisi manuale senza errori è impossibile.

Cosa posso offrire?

Se questo metodo ti sembra troppo lungo e faticoso, non hai superato tutte le fasi della disperazione con il precedente. Puoi provare a seguire i passaggi precedenti, ma se non aiuta, non dimenticare questa soluzione.

Alla ricerca della fuga di notizie, mi sono chiesto:Dove vengono creati gli oggetti che perdono? Era impossibile impostare punti di interruzione in tutti i punti in cui viene chiamata la funzione API. Inoltre, non ero sicuro che non accadesse nel .NET Framework o in una delle librerie di terze parti che utilizziamo. Pochi minuti di ricerca su Google mi hanno portato all'utilità API Monitor che ha permesso di registrare e tracciare le chiamate a tutte le funzioni del sistema. Ho facilmente trovato l'elenco di tutte le funzioni che generano oggetti GDI, individuato e selezionato in API Monitor. Quindi, ho impostato i punti di interruzione.

Successivamente, ho eseguito il processo di debug in Visual Studio e l'ho selezionato nell'albero dei processi. Il quinto breakpoint ha funzionato immediatamente:

Mi sono reso conto che sarei annegato in questo torrente e che avevo bisogno di qualcos'altro. Ho eliminato i punti di interruzione dalle funzioni e ho deciso di visualizzare il registro. Ha mostrato migliaia di chiamate. È diventato chiaro che non sarò in grado di analizzarli manualmente.

Il compito è Trovare le chiamate delle funzioni GDI che non causano l'eliminazione . Il registro conteneva tutto ciò di cui avevo bisogno:l'elenco delle chiamate di funzione in ordine cronologico, i valori restituiti e i parametri. Pertanto, avevo bisogno di ottenere un valore restituito della funzione Create%SOME_GDI_OBJECT% e trovare la chiamata di DeleteObject con questo valore come argomento. Ho selezionato tutti i record in API Monitor, li ho inseriti in un file di testo e ho ottenuto qualcosa di simile a CSV con il delimitatore TAB. Ho eseguito VS, dove intendevo scrivere un piccolo programma per l'analisi, ma prima che potesse caricarsi, mi è venuta in mente un'idea migliore:esportare i dati in un database e scrivere una query per trovare ciò di cui ho bisogno. È stata la scelta giusta poiché mi ha permesso di porre rapidamente domande e ottenere risposte.

Esistono molti strumenti per importare dati da CSV a un database, quindi non mi dilungo su questo argomento (mysql, mssql, sqlite).

Ho la seguente tabella:

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
)

Ho scritto la seguente funzione MySQL per ottenere il descrittore dell'oggetto eliminato dalla chiamata 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

E infine, ho scritto una query per individuare tutti gli oggetti correnti:

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

(Fondamentalmente, troverà semplicemente tutte le chiamate Elimina per tutte le chiamate Crea).

Come puoi vedere dall'immagine sopra, tutte le chiamate senza una singola Elimina sono state trovate contemporaneamente.

Quindi, l'ultima domanda è stata lasciata:come determinare, da dove vengono chiamati questi metodi nel contesto del mio codice? E qui un trucco stravagante mi ha aiutato:

  1. Esegui l'applicazione in VS per il debug
  2. Trovalo in Api Monitor e selezionalo.
  3. Seleziona una funzione richiesta nell'API e posiziona un punto di interruzione.
  4. Continua a fare clic su "Avanti" finché non verrà chiamato con i parametri in questione (ho davvero perso i punti di interruzione condizionali da VS)
  5. Quando arrivi alla chiamata richiesta, passa a CS e fai clic su Interrompi tutto .
  6. VS Debugger verrà interrotto proprio dove viene creato l'oggetto che perde e tutto ciò che devi fare è scoprire perché non viene eliminato.

Nota:il codice è scritto a scopo illustrativo.

Riepilogo:

L'algoritmo descritto è complicato e richiede molti strumenti, ma ha dato il risultato molto più velocemente rispetto a una ricerca stupida attraverso l'enorme base di codice.

Ecco un riepilogo di tutti i passaggi:

  1. Cerca perdite di memoria degli oggetti wrapper GDI.
  2. Se esistono, eliminali e ripeti il ​​passaggio 1.
  3. Se non ci sono perdite, cerca esplicitamente le chiamate alle funzioni API.
  4. Se la loro quantità non è grande, cerca uno script in cui un oggetto non viene eliminato.
  5. Se la loro quantità è grande o difficilmente possono essere rintracciati, scarica API Monitor e configuralo per la registrazione delle chiamate delle funzioni GDI.
  6. Esegui l'applicazione per il debug in VS.
  7. Riproduce la perdita (inizializzerà il programma per nascondere gli oggetti incassati).
  8. Connettiti con API Monitor.
  9. Riproduci la perdita.
  10. Copia il log in un file di testo, importalo in qualsiasi database a portata di mano (gli script presenti in questo articolo sono per MySQL, ma possono essere facilmente adottati per qualsiasi sistema di gestione di database relazionali).
  11. Confronta i metodi Crea ed Elimina (puoi trovare lo script SQL in questo articolo sopra) e trova i metodi senza le chiamate Elimina.
  12. Imposta un punto di interruzione in API Monitor alla chiamata del metodo richiesto.
  13. Continua a fare clic su Continua finché il metodo non viene chiamato con parametri riacquisiti.
  14. Quando il metodo viene chiamato con i parametri richiesti, fai clic su Break All in VS.
  15. Scopri perché questo oggetto non è stato eliminato.

Spero che questo articolo ti sia utile e ti aiuti a risparmiare tempo.