De câte ori nu vi s-a întâmplat să aveți un o problemă în producție sau în mediul de testare pe care să nu o puteți reproduce pe mașina de dezvoltare? Când acest lucru se întâmplă, lucrurile ajung să o ia razna, iar noi încercăm diferite modalități de remote debug. Fără să știm, aceste tool-uri pot să fie chiar lângă noi, însă noi le ignorăm sau nu știm cum să le folosim.
În cadrul acestui articolul voi prezinta diferite modalități prin care putem să facem debug fără să fie nevoie să folosim Visual Studio.
Deși Visual Studio este un produs extrem de bun, care ne ajută când avem nevoie să descoperim bug-uri și să facem debug, acesta nu o să ne fie de foarte mare folos în producție. În momentul în care avem un bug în producție, regulile jocului se schimbă. În producție aplicația este compilată pentru release, iar debug-ul nu mai este posibil.
În momentul în care nu putem să reproducem problema pe mașinile de dezvoltare. Orice am face nu putem să reproducem problema pe care o avem. Neputând reproduce problema este ca și cum am căuta acul în carul cu fân.
Dacă întâmplător apare problema, dar fără să avem un scenariu de reproducere, ajungem în același caz amintit mai sus.
Un alt caz ar fi atunci când memoria ocupată de aplicația noastră crește în timp, fenomenul manifestând-se doar la mașinile de producție. Putem doar să bănuim problema, dar nu știm cauza exactă. Din această cauză, putem să ajungem să "reparăm" cu totul alte zone din cod.
În general avem la îndemână două variante. Prima variantă se bazează în totalitate pe log-uri. Prin intermediul log-urilor putem să identificăm zonele din aplicație care nu funcționează corespunzător. Dar folosirea log-urilor poate să fie cu două tăișuri. Este nevoie să știi ce este necasar să apară în log-uri și cât de des. În caz contrar te poți trezi cu mii de pagini de log-uri nefolositoare și aproape imposibil de analizat. Dacă ajungem să avem prea multe log-uri putem să fim surprinși de schimbarea comportamentului aplicației.
În cazul în care este posibil putem să trimitem PDB-urile pe mașina de producție. Prin acest mod vom avea acces la tot stack trace-ul pe care o excepție îl generează.
Log-urile ne pot fi de mare ajutor pentru a rezolva diferite probleme care apar în producție. Chiar dacă log-urile sunt foarte folositoare, nu ne vor ajuta de fiecare dată. Există diferite probleme care pot să apară și care să fie extrem de greu de identificat folosind log-urile. De exemplu un dead-lock ar fi aproape imposibil de identificat prin intermediul log-urilor.
O altă variantă pe care o avem la dispoziție este crearea de memory dump-uri și analizarea acestora.
Un memory dump este un snapshot a procesului într-un anumit moment. Pe lângă informațiile despre alocarea memoriei, un snapshot conține informații despre starea diferitelor thread-uri, obiecte și cod. Folosind această informație putem să obținem informații foarte valoroase despre procesul care rulează. Acest snapshot reprezintă imaginea memoriei în format 32 sau 64 de biti, în funcție de sistem.
În general există două tipuri de memory dump. Primul este minidump. Acesta este cel mai simplu memory dump care se poate face și conține doar informații despre stack - starea procesului sau despre ce apeluri se fac și așa mai departe.
Al doilea tip de memory dump este full dump. Acesta conține toate informațiile care se pot obține, incluzând un snapshot la memorie. Timpul necesar pentru obținerea unui full dump este mult mai mare în comparație cu un minidump, iar fișierul de dump în sine este mult mai mare.
Există diferite aplicații care ne ajută să facem acest lucru. Unele din acestea ne permit să facem un dump în mod automat în funcție de diferiți parametri.
În cazul în care este nevoie să facem un memory dump cea mai simplă soluție este din Task Manager. Tot ce este nevoie să facem este să dăm click dreapta pe un proces și să selectăm "Create dump file". Același lucru îl putem face folosind Visual Studio sau "adplus.exe". Ultima variantă este un tool de debug pentru Windows care se regăsește pe aproape toate mașinile care rulează Windows.
În următorul exemplu specificăm la adplus să ne creeze un memory dump în acest moment:
adplus -hang -o C:myDump -pn MyApp.exe
Prin intermediul opțiunii pn specificăm numele procesului pentru care dorim să creăm un dump. În cazul în care dorim să creăm un dump în mod automat putem să folosim opțiunea -crash.
adplus -crash -o C:myDump -pn MyApp.exe
adplus -crash -o C:myDump -sc MyApp.exe
Dacă este nevoie să creăm un dump în mod automat pe lângă "adplus.exe" putem să folosim DebugDiag și "clrdmp.dll". Cele trei opțiuni pe care le avem pentru crearea unui dump în mod automat sunt destul de similare. DebugDump ne permite să configurăm sistemul ca să creeze automat un memory dump în momentul în care nivelul CPU-ului este mai mare de X% într-un anumit interval de timp.
Pe lângă aceste tool-uri există multe altele pe piață. În funcție de necesități puteți să folosiți orice tool de acest tip.
Debugger-ul nativ pentru un dump este reprezentat de Windbg. Acesta este un tool de puternic, cu care se pot obține informații foarte prețioase. Singura problemă a acestui tool este că nu este prietenos. Vom vedea puțin mai târziu care sunt alternativele la Windbg. Trebuie să ținem cont că în aproape toate cazurile alternativele la Windbg folosesc în spate acest debugger - doar că expun o interfață mai prietenoasă și mai utilă.
O alternativă la Windbg este orice Visual Studio mai recent decât Visual Studio 2010. Începând cu Visual Studio 2010, acesta ne oferă posibilitatea să analizăm dump-urile pentru .NET 4.0+. Ceea ce putem să facem în Visual Studio nu este la fel de avansat în comparație cu ceea ce ne permite Windbg, dar în general poate să ne fie de ajuns.
Primul pas pe care trebuie să îl facem după ce deschidem Windbg este să încărcăm un dump (CTRL+D). Odată încărcat, un dump poate să fie vizualizat din diferite moduri. De exemplu putem să analizăm thread-urile, memoria, resursele alocate și așa mai departe.
Pentru a putea face mai mult, de exemplu să vizualizăm și să analizăm codul managed avem nevoie să încărcăm librării ajutătoare precum Son of Strike (SOS) sau Son of Strike Extension (SOSEX). Aceste două librări ne deschid noi uși, putând să analizeze datele din dump într-un mod extrem de folositor.
SOS ne permite să vizualizăm procesul în sine. Ne permite să accesăm obiectele thread-urile și informațiile din garbage colector. Putem să vizualizăm inclusiv nume de variabile și valoarea acestora.
Trebuie știut că toate informațiile care se pot accesa fac parte din managed memory. Din această cauză, SOS este strâns legat de CLR și de versiunea acestuia. În momentul în care încărcăm modulul SOS, trebuie să avem grijă să îl încărcăm pe cel corespunzător pentru versiunea de .NET a aplicației noastre.
.loadby sos mscorks
.loadby sos clr
În exemplele de mai sus am încărcat modulul de SOS pentru .NET 3.5-, iar în al doilea exemplu am încărcat SOS pentru .NET 4.0+.
Toate comenzile SOS încep cu "!". Comanda de bază este "!help". În cazul în care dorim să vizualizăm lista de thread-uri putem să ne folosim de comanda "!threads" care are un output asemănător cu următorul:
0:000> !threads
ThreadCount: 5
UnstartedThread: 0
BackgroundThread: 2
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
Lock
ID OSID ThreadOBJ Count Apt Exception
…
Pănă acuma am văzut că avem la dispoziție multe tool-uri pentru a crea și a analiza un dump. Acuma a venit momentul să vedem ce trebuie să facem pentru a putea analiza un crash.
1. Lansăm procesul
2. Înainte "să crape", comandăm adplus-ului să creeze un dump în momentul în care procesul "crapă"
adplus -crash -pn [numeProcesor]
3. Lansăm Windbg (dupa crash),
3.1. Încărcăm dump-ul,
3.2. Încărcăm SOS,
3.3. !threads (pentru a vedea ce thread a crăpat),
3.4. !PrintException (pe thread-ul care a crăpat pentru a vedea excepția),
3.5. !clrstack (pentru a vedea stack-ul de apeluri),
3.6. !clrstack -a (pentru a vedea stack-ul împreună cu parametri),
3.7. !DumpHeap -type Expcetion (listeză toate excepțiile care nu sunt legate de GC).
Trebuie știut că rezultatele sunt în funcție de modul în care aplicația este compilată. De exemplu, dacă s-a făcut optimizare de cod în momentul compilării. Totodată lista de excepții pe care o putem obține poate să fie destul de lungă din cauza unor comenzi precum !DumpHeap ne returnează toate excepțiile produse - chiar și cele pre-create precum ThreadAbord.
Un deadlock apare în momentul în care două sau mai multe thread-uri așteaptă dupa aceeași resursă. În aceste cazuri o parte din aplicație dacă nu chiar toată aplicația se blochează.
Pentru acest caz primul pas este să creăm un dump folosind comanda:
Addplus -hang -o -c:myDump -pn [NumeProces]
Apoi va fi nevoie să analizăm stack trace-ul pentru fiecare thread și să vedem dacă este blocat (Monitor.Enter, ReadWriteLock.Enter…). Odată ce am identificat aceste thread-uri putem să găsim resursele folosite de fiecare thread, împreună cu thread-ul care ține blocate aceste resurse.
Pentru acești ultimi pași comanda "!syncblk" ne vine în ajutor. Aceasta ne listează blocurile de memorie pentru un anumit thread.
În cadrul acestui articol am descoperit cum putem să creăm un dump și care sunt tool-urile de bază pentru a-l analiza. Prin intermediul fișierelor dump putem să accesăm informația pe care nu am putea-o accesa în mod normal. Unele date pot să fie accesate doar prin aceste dump-uri și nu prin alte moduri (debug din Visual Studio).
Am putea afirma că aceste tool-uri sunt puternice, dar sunt destul de greu de folosit, necesitând o curbă de învățare destul de mare.