SELECT * FROM CODE WHERE SHIT HAPPENS

March 10th, 2010

For lang tid siden fik jeg tilbud om en licens til NDepend mod en omtale af produktet på min blog. Diverse vigtigere opgaver så som at blive far for anden gang tog derimod tiden fra projektet, og jeg fik således ikke gjort noget ved. I forbindelse med den nyeste version af NDepend var Patrick Smacchia, som står bag produktet, dog så flink at kippe med flaget endnu en gang og tak for det. NDepend er bestemt et kig værd også uden en gratis licens.

NDepend er et værktøj, der kan analysere .NET assemblies. Navnet antyder, at det primært er analyse af afhængigheder mellem assemblies, der er NDepends force, men tag ikke fejl, NDepend kan så meget mere end det.

I gang med NDepend

Det er let at komme i gang med NDepend. Vi kan enten lave et selvstædigt projekt og importere relevante assemblies, eller vi kan udnytte NDepends integration med Visual Studio og koble det på en solution.

Derefter analyserer NDepend de valgte assemblies. Den første rapport er en liste over hvilke assemblies, de valgte assemblies afhænger af. Hvis der er mangler på denne liste, risikerer vi fejl under afvikling af applikationen.

Vi kan nu udføre en grundigere analyse, der kigger de valgte assemblies igennem og opsamler informationer om afhængigheder og kodekonstruktioner.

NDepend

Afhængighederne mellem de forskellige assemblies kan visualiseres på forskellige måder. En matrice giver det store overblik, og en graf viser, hvordan de enkelte komponenter bruger hinanden. Begge dele er interaktive, og vi kan således grave os ned i detaljerne efter behov. Meget brugbart.

Der er også den underlige månelandskabsoversigt, som man ofte ser afbilledet i forbindelse med omtale af NDepend. Den havde jeg indledningsvis lidt svært ved at se ideen med. Den vender vi tilbage til.

Selvom oversigterne er særdeles brugbare, så er det CQL, der står som den største killer feature ved NDepend for mit vedkommende. CQL er et SQL-lignende sprog til at lave forespørgsler mod koden. Vi kan således få vist alle metoder med mere end et bestemt antal linjer, alle metoder med mere end et bestemt antal parametre og så videre. Det er let at filtrere bestemte namespaces, typer og så videre fra, så på ingen tid kan vi få skruet meget detaljeret og specifik rapportering om vores kode sammen. Nedenstående forespørgsel finder f.eks. alle metoder med mere end 30 linjers kode:

SELECT METHODS WHERE
  NbILInstructions > 30
  ORDER BY NbILInstructions DESC

Vi kan bruge AND og OR og så videre, og kender man lidt SQL, er det let at blive dus med syntaksen. Hastigheden er et kapitel for sig. Selv med mange assemblies får man resultaterne med det samme.

Desværre tillader den nuværende syntaks ikke joins mellem de forskellige kilder, så det er f.eks. ikke mulig først at udvælge et antal typer og derefter undersøge deres metoder. Vi kan komme ud om nogle af begrænsningerne ved at specificere yderligere where-betingelser, men en egentlig join ville give lidt ekstra.

Her kommer månelandskabsgrafen til sin ret. Når vi laver en forespørgsel, farver NDepend de relevante områder af denne graf afhængig af hvor mange metoder i de givne assemblies, der opfylder vores forespørgsel. Jo mere farve, desto flere hits så at sige. Det gøre det meget let, at finde ud af hvilke assemblies, der er mest berørt af vores forespørgsel.

Den specielle graf giver mening med CQL

Forespørgslerne kan tilmed laves til regler, således at de kan inkluderes som en del af vores build job, og derved kan vi få rapporteret uhensigtsmæssigheder på samme måde, som vi kender det fra eksempelvis FxCop.

En sammenligning af de FxCop og NDepend er nærliggende, for med NDepends CQL-baserede regler, er der et vist overlap de to imellem. I modsætning til FxCop er NDepend dog utrolig let at komme i gang med. Der er få, men gode regler, og derfor får man ret brugbar information out of the box. Min erfaring med FxCop er, at det kan være svært at indføre på eksisterende projekter, fordi der simpelthen er så mange regler og muligheder, og man ender hurtig med at bruge lang tid på at få noget brugbart ud af det. Det er som sagt ikke tilfældet med NDepend.

Man kommer hurtig i gang og på grund af det geniale forespørgselssprog er det let, at tilpasse produktet til de aktuelle behov.

I like it!

Hallo debugger! Er du der?

March 9th, 2010

Debugger-klassen i System.Diagnostics, har en property ved navn IsAttached, der ikke overraskende kan fortælle os, om vores proces kører under en debugger. Hjælpeteksten nævner dog ikke noget om, at der skal være tale om en managed debugger. Det vil sige, at IsAttached f.eks. ikke opdager tilstedeværelsen af WinDbg.

Heldigvis er der et Win32-kald, der kan fortælle os om en native debugger er til stede. For at få svar på det, skal følgende kodestump kaldes:

[DllImport("kernel32.dll")]
static extern bool IsDebuggerPresent()

Desværre insisterer dette kald på, at det skal være en native debugger, så den opdager ikke Visual Studio, hvis der er tale om debugging af en .NET applikation.

Ny version af Debugging Tools

March 8th, 2010

Jeg plejer ikke at annoncere hver eneste nye version af Debugging Tools for Windows, men denne gang vil jeg gøre en undtagelse. Version 6.12.2.633 er netop frigivet (faktisk er det nogle dage siden, men lad os ikke hænge os i det).

Det er to ændringer, der gør denne release nævneværdig. For det første er Debugging Tools ikke længere en selvstændig pakke. Den er nu en del af Windows Driver Kit (WDK). Det vil sige, i stedet for at downloade en lille fil, skal vi nu hente et ISO-image. Det er godt nok ikke særlig smart. Heldigvis indeholder ISO-en både 32 og 64 bit versionerne af Debugging Tools.

Den anden ændring er heldigvis mere positiv. Adplus er skrevet helt om, så i stedet for det enorme VB-script vi er vant til, har vi nu fået en managed applikation – adplus.exe. Den kræver .NET 2.0. Skulle man have brug for den gamle version af Adplus ligger den stadig med installationen, men den er blevet omdømt til adplus_old.vbs.

Der er også kommet en AdPlusManager.exe til debugging i distribuerede miljøer.

Der medfølger desuden et Word-dokument, der beskriver de nye versioner.

Flere gode debugging-nyheder i CLR 4

February 24th, 2010

Læser man hjælpen til den nye version af SOS, er det første man lægger mærke til nok de mange nye kommandoer, men der er faktisk også meget interessante opdateringer til den eksisterende funktionalitet.

Hjælpeteksten til !u-kommandoen er således blevet opdateret med følgende lille kommentar:

If the debugger has the option SYMOPT_LOAD_LINES specified (either by the.lines or .symopt commands), and if symbols are available for the managed module containing the method being examined, the output of the command will include the source file name and line number corresponding to the disassembly.

Vi har med andre ord fået en smule af funktionaliteten fra den legendariske version 6.7.5 tilbage. Det er desværre ikke en komplet løsning, men det er i hvert fald et seriøst skridt i den rigtige retning.

Heldigvis er det ikke kun !u, der er blevet opdateret. !clrstack viser nu også linjenumre, hvis ovenstående betingelser er på plads. Vi har således fået meget bedre muligheder, for at illustrere sammenhængen den kørende applikation og kildeteksten. Lad os se på et eksempel.

static void SomeMethod(int i) {
    SomeSubMethod(i + 1);
}

static void SomeSubMethod(int i) {
    var text = string.Format("The answer is {0}", i);
    Console.WriteLine(text);
    Console.ReadLine();
}

static void Main() {
    SomeMethod(41);
}

Sætter vi WinDbg på ved Console.ReadLine(), og udskriver stakken for den relevante tråd, får vi følgende:

0:000> !clrstack
OS Thread Id: 0x1b00 (0)
Child SP         IP               Call Site
000000000019e648 00000000772c00da [NDirectMethodFrameStandalone: 000000000019e648] System.IO.__ConsoleStream.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
000000000019e5f0 000007feeb1d6311 DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)*** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v4.0.30128_64\mscorlib\efe2adda88bca3a9f9bf9cc89514729b\mscorlib.ni.dll

000000000019e710 000007feeb980e7a System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Int32, Int32 ByRef)
000000000019e780 000007feeb980ce2 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
000000000019e7e0 000007feeb19601c System.IO.StreamReader.ReadBuffer()
000000000019e830 000007feeb194c40 System.IO.StreamReader.ReadLine()
000000000019e890 000007feeb988d90 System.IO.TextReader+SyncTextReader.ReadLine()
000000000019e8f0 000007ff0015023e TestApp.Program.SomeSubMethod(Int32)
 [C:\dev2010\TestApp\TestApp\Program.cs @ 18]
000000000019e950 000007ff00150199 TestApp.Program.SomeMethod(Int32) [C:\dev2010\TestApp\TestApp\Program.cs @ 12]
000000000019e980 000007ff00150144 TestApp.Program.Main() [C:\dev2010\TestApp\TestApp\Program.cs @ 22]
000000000019ed70 000007feec05cc74 [GCFrame: 000000000019ed70]

Læg mærke til fil- og linjeangivelserne for Main(), SomeMethod() og SomeSubMethod().

Via instruktionspointeren, kan vi vise den oversatte kode for metoderne. Lad os se nærmere på SomeSubMethod().

0:000> !U  000007ff0015023e
Normal JIT generated code
TestApp.Program.SomeSubMethod(Int32)
Begin 000007ff001501c0, size 8b

C:\dev2010\TestApp\TestApp\Program.cs @ 15:
000007ff`001501c0 894c2408        mov     dword ptr [rsp+8],ecx
000007ff`001501c4 4883ec58        sub     rsp,58h
000007ff`001501c8 48c744242000000000 mov   qword ptr [rsp+20h],0
000007ff`001501d1 48b8d8340300ff070000 mov rax,7FF000334D8h
000007ff`001501db 8b00            mov     eax,dword ptr [rax]
000007ff`001501dd 85c0            test    eax,eax
000007ff`001501df 7405            je      000007ff`001501e6
000007ff`001501e1 e88ada33ec      call    clr!JIT_DbgIsJustMyCode (000007fe`ec48dc70)
000007ff`001501e6 90              nop

C:\dev2010\TestApp\TestApp\Program.cs @ 16:
000007ff`001501e7 48b85030591200000000 mov rax,12593050h
000007ff`001501f1 488b00          mov     rax,qword ptr [rax]
000007ff`001501f4 4889442430      mov     qword ptr [rsp+30h],rax
000007ff`001501f9 8b442460        mov     eax,dword ptr [rsp+60h]
000007ff`001501fd 89442428        mov     dword ptr [rsp+28h],eax
000007ff`00150201 488d0d888a15eb  lea     rcx,[mscorlib_ni+0x4f8c90 (000007fe`eb2a8c90)]
000007ff`00150208 488d542428      lea     rdx,[rsp+28h]
000007ff`0015020d e89e29edeb      call    clr!JIT_BoxFastMP_InlineGetThread (000007fe`ec022bb0)
000007ff`00150212 488bd0          mov     rdx,rax
000007ff`00150215 488b4c2430      mov     rcx,qword ptr [rsp+30h]
000007ff`0015021a e8116a01eb      call    mscorlib_ni+0x3b6c30 (000007fe`eb166c30) (System.String.Format(System.String, System.Object), mdToken: 0000000001C9C090)
000007ff`0015021f 4889442438      mov     qword ptr [rsp+38h],rax
000007ff`00150224 488b442438      mov     rax,qword ptr [rsp+38h]
000007ff`00150229 4889442420      mov     qword ptr [rsp+20h],rax

C:\dev2010\TestApp\TestApp\Program.cs @ 17:
000007ff`0015022e 488b4c2420      mov     rcx,qword ptr [rsp+20h]
000007ff`00150233 e8f80903eb      call    mscorlib_ni+0x3d0c30 (000007fe`eb180c30) (System.Console.WriteLine(System.String), mdToken: 0000000001C9C090)
000007ff`00150238 90              nop

C:\dev2010\TestApp\TestApp\Program.cs @ 18:
000007ff`00150239 e862886beb      call    mscorlib_ni+0xa58aa0 (000007fe`eb808aa0) (System.Console.ReadLine(), mdToken: 0000000001C9C090)
>>> 000007ff`0015023e 4889442440      mov     qword ptr [rsp+40h],rax
000007ff`00150243 90              nop

C:\dev2010\TestApp\TestApp\Program.cs @ 19:
000007ff`00150244 eb00            jmp     000007ff`00150246
000007ff`00150246 4883c458        add     rsp,58h
000007ff`0015024a c3              ret

Som sædvanlig får vi den JIT-oversatte kode tilsat CLR-kommentarer, men læg mærke til, at de enkelte kodeblokke nu indledes med en reference til den relevante linje i kildeteksten. Det gør det meget lettere, at skabe sammenhæng mellem den JIT-oversatte maskinekode og den oprindelige kildetekst.

Men vent! Der er mere!

Yderligere granskning af hjælpeteksten opsporer følgende lille, interessante notits i slutningen af FAQ-sektionen.

Does SOS support DML?
Yes. SOS respects the .prefer_dml option in the debugger. If this setting is turned on, then SOS will output DML by default. Alternatively, you may leave it off and add /D to the beginning of a command to get DML based output for it. Not all SOS commands support DML output.

Så SOS understøtter nu DML. Fint, men hvad er pokker DML?

DML står for debugger markup language. Det er ikke en ny feature, den har været der siden version 6.6.07, men det er en forholdsvis velbevaret hemmelighed. Således er der kun en enkelt reference til DML i hjælpeteksten og ingen uddybning. Den eneste information, jeg har fundet, er et dokument ved navn dml.doc i WinDbg installationsbibliotek.

Nyheden er altså ikke DML i sig selv, men at SOS nu understøtter DML.

Heldigvis kræver DML ikke den store forklaring. I en nøddeskal er DML et simpelt markup-sprog, der kan bruges til at tilføre yderligere information til resultatet af debugger-kommandoer. Som FAQen nævner, er flere af SOS-kommandoerne blevet udstyret med DML, så hvis vi slår DML-visning til, får vi altså yderligere funktionalitet i form af links til relevante kommandoer i forhold til det aktuelle output.

Kører vi eksempelvis !dumpobj, får vi, som vist nedenfor, links til flere relaterede opslag. Det gør en hel del arbejdsgange meget lettere.

DML i SOS

Effekten af de enkelte links kan aflures nederst til venstre i WinDbgs statusbar.

Udokumenterede kommandoer og forkortelser

Udover den umiddelbare reduktion af tastearbejdet giver DML også indblik i yderligere features. Jeg opdagede således .extmatch-kommandoen, da jeg kørte .chain med DML slået til.

.extmatch viser alle eksporterede kommandoer fra en given extension. Kører vi den på SOS, dukker der adskillige udokumenterede kommandoer op som f.eks. HandleCLRN, SOSFlush, VerifyStackTrace og WatsonBuckets. Uden dokumentation er det svært, at få noget fornuftig ud af disse. SOSFlush er godt nok beskrevet på MSDN, men det er ikke fordi, jeg blev meget klogere af det.

Derudover fandt jeg også en del udokumenterede forkortelser, jeg ikke kendte. De er som følger:

!da for !dumparray
!hof for !histobjfind
!t for !threads
!tp for !threadpool
!vh for !verifyheap
!vo for !verifyobj

Det eneste problem, jeg har opdaget med DML indtil videre, er, at mdTokens af en eller anden grund linker til !u med et ugyldigt -md-parameter. Jeg håber, at det bliver rettet inden release.

Hjælp til fejlsøgning af OutOfMemoryException

February 18th, 2010

Da jeg kom tilbage fra forældreorlov, blev jeg involveret i at fejlsøge nogle OutOfMemoryExceptions, en af mine kolleger havde oplevet. Det var ikke så meget det, at der kom exceptions, men af en eller anden grund væltede disse exceptions hele afviklingsmiljøet. Vores .NET-kode afvikles i en CLR indlæst i en anden proces. Problemet var således, at disse exceptions rev værtsprocessen med sig i faldet. Det var ikke særlig hensigtsmæssigt.

Desværre var det relativt omfattende at reproducere fejlen, så derfor satte jeg mig for, at forsøge at genskabe problemet under lidt enklere forhold. Det er jo heldigvis ikke særlig svært, at fremprovokere en OutOfMemoryException, så jeg skulle bare finde en lille applikation i vores framework, som jeg kunne lade det gå ud over. Ikke længe efter kunne jeg således reproducere problemet, og så var det jo bare at finde ud af, hvorfor vores almindelige fejlhåndtering af en eller anden grund var sat ud af spil.

Jeg fyrede op under min applikation og koblede WinDbg på sagen. Hvor svært kunne det være. Desværre opførte applikationen sig eksemplarisk i selskab med WinDbg. Godt nok fik jeg en OutOfMemoryException som forventet, men vores fejlhåndtering virkede pludselig efter hensigten.

Min første tanke var således, at der var en eller anden sindrig race condition i fejlhåndteringen. En debugger ændrer som bekendt timingen i afviklingen, så det kunne jo være, at WinDbg introducerede den lille forskel, der var afgørende for om det virkede eller ej. Det måtte undersøges.

Jeg læste en masse kode igennem, men kunne ikke finde evidens for at der skulle være en race condition. Så debuggede jeg noget mere, forstyrrede et par kolleger, rev mig lidt i håret og var i det hele taget ikke just springfuld af ideer til, hvorfor pokker det opførte sig fint, så snart jeg pudsede WinDbg på problemet.

Efter at have spildt en rum tid med forskellige mindre undersøgelser, kastede jeg mig over Mario Hewardts seneste bog Advanced .NET Debugging. Den havde jeg læst lige inden, jeg gik på orlov, og jeg erindrede, at der vist var et afsnit om debugging af OutOfMemoryExceptions.

Bogen beskriver blandt andet en interessant registry setting, der kan bruges til hjælp af debugging af netop OutOfMemoryExceptions.

Sætter vi

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\GCBreakOnOOM (DWORD)

til andet end 0, vil afviklingsmiljøet indskyde en break-instruktion i applikationen ved det første tegn på out of memory. Bogen beskriver godt nok flere værdier, men jeg kan kun konstatere at mulighederne er nul og ikke nul. Står nøglen til andet end nul, indskyder CLRen således et debug break, lige så snart den konstaterer out of memory.

Denne setting får altså afviklingsmiljøet til at opføre sig helt anderledes, og jeg havde ganske rigtigt pillet ved den, inden jeg gik på orlov og derefter glemt alt om det. Det forklarede, hvorfor problemet ikke forekom, når WinDbg var koblet på processen. I det tilfælde stoppede applikationen blot, og jeg kunne fortsætte, som om intet var hændt. Vores værtsproces havde det til gengæld ikke særlig godt med en umotiveret break-instruktion, og det var forklaringen på, hvorfor jeg kunne hive den ned med en OutOfMemoryException.

Men det forklarede jo ikke, hvorfor min kollega, der oprindelig havde rapporteret dette problem, så denne opførsel. Jeg blev dog nødt til at afklare, hvorvidt han eventuelt skulle have pillet ved denne eksotiske setting (en søgning giver ikke mange hits, så jeg regnede ikke med, at han var faldet over den ved et tilfælde, men jeg kunne jo lige så godt spørge). Til min overraskelse viste det sig, at han faktisk havde sat GCBreakOnOOM! Under min orlov havde han været forbi min plads og var faldet over Hewardts bog, som han havde lånt. Ved et tilfælde havde han læst om OutOfMemoryExceptions, set den omtalte setting, oprettet den i sit registry og derefter glemt alt om det.

Så hang tingene sammen igen, men hvad er chancerne for, at min kollega og jeg ikke alene begge sætter denne setting men også glemmer alt om det efterfølgende? Jeg er sikker på, at der er en relevant lektie et eller andet sted i denne oplevelse.

Lad mig afrunde dette indlæg med et par noter omkring GCBreakOnOOM.

Som nævnt får denne registry setting, afviklingsmiljøet til at opføre sig specielt i tilfældet af out of memory. Lige så snart CLRen opdager, at den skal til at smide en OutOfMemoryException, indskyder den en break-instruktion. Lad os se på et eksempel.

private static void Main(string[] args) {
    GenerateOOM();
}

private static void GenerateOOM() {
   var list = new List<double?>(10000000);
   while (true) {
       list.Add(0);
   }
}

Hvis vi kobler WinDbg på efter at have kørt ovenstående, vil vi således ikke se en exception, idet break-instruktionen indskydes inden afviklingsmiljøet smider OutOfMemoryException.

0:000> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
                                              PreEmptive                                                Lock
       ID OSID        ThreadOBJ     State   GC     GC Alloc Context                  Domain           Count APT Exception
   0    1  b60 0000000000c63350      a020 Enabled  0000000002941c70:0000000002943a60 0000000000c5a360     0 MTA
   2    2  fc4 0000000000c6b420      b220 Enabled  0000000000000000:0000000000000000 0000000000c5a360     0 MTA (Finalizer)

Ingen exception her. Lad os se på vores stak (bemærk desuden, at vi befinder os på den relevante tråd, da vi har indskudt en break-instruktion på denne).

0:000> !clrstack
OS Thread Id: 0xb60 (0)
Child-SP         RetAddr          Call Site
000000000028e710 000007ff00170336 System.Collections.Generic.List`1[[System.Nullable`1[[System.Double, mscorlib]], mscorlib]].set_Capacity(Int32)
000000000028e760 000007ff001701d2 System.Collections.Generic.List`1[[System.Nullable`1[[System.Double, mscorlib]], mscorlib]].Add(System.Nullable`1<Double>)
000000000028e7a0 000007ff00170129 TestBench.Program.GenerateOOM()
000000000028e800 000007feefa3d502 TestBench.Program.Main(System.String[])

Læg mærke til at set_Capacity ligger øverst på stakken, så det er altså den operation, vi er i gang med. set_Capacity er en hyppig årsag til out of memory. Det er nemlig her, at List<T>s underliggende array bliver udvidet med en ny af dobbelt størrelse, og derfor vil det ofte være den direkte årsag til out of memory. Lad os se om vores native stak giver yderligere detaljer.

0:000> k
Child-SP          RetAddr           Call Site
00000000`0028e338 000007fe`efc2dd33 KERNELBASE!DebugBreak+0x2
00000000`0028e340 000007fe`efc2dda3 mscorwks!`string'+0x55a93
00000000`0028e3b0 000007fe`ef916df3 mscorwks!`string'+0x55b03
00000000`0028e3e0 000007fe`efe65c79 mscorwks!AllocateArrayEx+0x323
00000000`0028e530 000007ff`00170437 mscorwks!JIT_NewArr1+0x239
00000000`0028e710 000007ff`00170336 0x7ff`00170437
00000000`0028e760 000007ff`001701d2 0x7ff`00170336
00000000`0028e7a0 000007ff`00170129 0x7ff`001701d2
00000000`0028e800 000007fe`efa3d502 0x7ff`00170129
00000000`0028e830 000007fe`ef8f9fd3 mscorwks!CallDescrWorker+0x82
00000000`0028e880 000007fe`ef90a3af mscorwks!CallDescrWorkerWithHandler+0xd3
00000000`0028e920 000007fe`ef87dc7f mscorwks!MethodDesc::CallDescr+0x24f
00000000`0028eb70 000007fe`ef861c74 mscorwks!ClassLoader::RunMain+0x22b
00000000`0028edd0 000007fe`ef899955 mscorwks!Assembly::ExecuteMainMethod+0xbc
00000000`0028f0c0 000007fe`ef9adb07 mscorwks!SystemDomain::ExecuteMainMethod+0x491
00000000`0028f690 000007fe`ef86855c mscorwks!ExecuteEXE+0x47
00000000`0028f6e0 000007fe`f3964989 mscorwks!CorExeMain+0xac
00000000`0028f740 000007fe`f3a05b21 mscoreei!CorExeMain+0x41
00000000`0028f770 00000000`7716f56d MSCOREE!CorExeMain_Exported+0x57
00000000`0028f7a0 00000000`772a3281 KERNEL32!BaseThreadInitThunk+0xd

Ej heller her er der spor af en exception, men vi kan se, at AllocateArrayEx bliver kaldt. Vi er med andre ord kommet ind før CLRen har kastet sin OutOfMemoryException. I debugging-terminologi er dette før first chance exception. Vi har altså alle muligheder for at inspicere tilstanden af applikationen inden the shit hits the fan så at sige.

Det vil også sige, at hvis vi kører videre, vil vi få den ventede exception.

0:000> g
(484.b60): C++ EH exception - code e06d7363 (first chance)
(484.b60): CLR exception - code e0434f4d (first chance)
(484.b60): CLR exception - code e0434f4d (!!! second chance !!!)
KERNELBASE!RaiseException+0x39:
000007fe`fd49aa7d 4881c4c8000000  add     rsp,0C8h

Bingo! Eftersom vi ikke gør noget for at fange exceptions, fortsætter koden med både first og second chance.

Herefter har vi vores exception, og debugging af problemet fra dette punkt svarer til det, vi ville se, hvis vi havde sat Adplus til at lave et crash dump.

0:000> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
                                              PreEmptive                                                Lock
       ID OSID        ThreadOBJ     State   GC     GC Alloc Context                  Domain           Count APT Exception
   0    1  b60 0000000000c63350      a020 Enabled  0000000002941e90:0000000002943a60 0000000000c5a360     0 MTA System.OutOfMemoryException (0000000002941c70)
   2    2  fc4 0000000000c6b420      b220 Enabled  0000000000000000:0000000000000000 0000000000c5a360     0 MTA (Finalizer)

Som forventet finder vi en OutOfMemoryException på vores hovedtråd:

0:000> !pe
Exception object: 0000000002941c70
Exception type: System.OutOfMemoryException
Message: <none>
InnerException: <none>
StackTrace (generated):
    SP               IP               Function
    000000000028E710 000007FF00170438 mscorlib_ni!System.Collections.Generic.List`1[[System.Nullable`1[[System.Double, mscorlib]], mscorlib]].set_Capacity(Int32)+0x48
    000000000028E760 000007FF00170337 mscorlib_ni!System.Collections.Generic.List`1[[System.Nullable`1[[System.Double, mscorlib]], mscorlib]].Add(System.Nullable`1<Double>)+0x27
    000000000028E7A0 000007FF001701D3 TestBench!TestBench.Program.GenerateOOM()+0x83
    000000000028E800 000007FF0017012A TestBench!TestBench.Program.Main(System.String[])+0xa

StackTraceString: <none>
HResult: 8007000e

Kigger vi på native stak, finder vi ligeledes de relevante kald i mscorwks og derefter kernelbase til håndtering af exceptions.

0:000> k
Child-SP          RetAddr           Call Site
00000000`0028e330 000007fe`ef8cf0bd KERNELBASE!RaiseException+0x39
00000000`0028e400 000007fe`efa1e38f mscorwks!RaiseTheExceptionInternalOnly+0x295
00000000`0028e4d0 000007fe`efe65cca mscorwks!UnwindAndContinueRethrowHelperAfterCatch+0x63
00000000`0028e530 000007ff`00170437 mscorwks!JIT_NewArr1+0x28a
00000000`0028e710 000007ff`00170336 0x7ff`00170437
00000000`0028e760 000007ff`001701d2 0x7ff`00170336
00000000`0028e7a0 000007ff`00170129 0x7ff`001701d2
00000000`0028e800 000007fe`efa3d502 0x7ff`00170129
00000000`0028e830 000007fe`ef8f9fd3 mscorwks!CallDescrWorker+0x82
00000000`0028e880 000007fe`ef90a3af mscorwks!CallDescrWorkerWithHandler+0xd3
00000000`0028e920 000007fe`ef87dc7f mscorwks!MethodDesc::CallDescr+0x24f
00000000`0028eb70 000007fe`ef861c74 mscorwks!ClassLoader::RunMain+0x22b
00000000`0028edd0 000007fe`ef899955 mscorwks!Assembly::ExecuteMainMethod+0xbc
00000000`0028f0c0 000007fe`ef9adb07 mscorwks!SystemDomain::ExecuteMainMethod+0x491
00000000`0028f690 000007fe`ef86855c mscorwks!ExecuteEXE+0x47
00000000`0028f6e0 000007fe`f3964989 mscorwks!CorExeMain+0xac
00000000`0028f740 000007fe`f3a05b21 mscoreei!CorExeMain+0x41
00000000`0028f770 00000000`7716f56d MSCOREE!CorExeMain_Exported+0x57
00000000`0028f7a0 00000000`772a3281 KERNEL32!BaseThreadInitThunk+0xd
00000000`0028f7d0 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

Exception eller ej

February 15th, 2010

Mark Seemann stillede for nylig et spørgsmål på Twitter, der lød noget i retning af ”Skal Delete(int id) kaste en exception hvis det element, id repræsenterer, ikke eksisterer, eller skal den bare undlade at gøre noget?”.

Ræsonnementet for ikke at gøre noget er naturligvis, at hvis vi beder om at få slettet noget, og det ikke er der, så er sluttilstanden jo den ønskede, uanset om vi slettede noget eller ej.

Mit svar var, at med den signatur skulle metoden kaste en exception, og i dette indlæg vil jeg forsøge at uddybe lidt. Der er to grunde til, at jeg synes, en exception er på sin plads i dette tilfælde.

For det første er vi vant til, at Delete() på framework-typer som f.eks. File, Directory og DataRow alle smider passende exceptions, hvis de kaldes med et element, der ikke findes. Så hvis vi vil holde os til Principle of least surprise, er en exception det rette valg.

For det andet, og måske vigtigere, vil det være umuligt for den kaldende kode at afgøre, hvorvidt operationen var en succes eller ej, hvis Delete() ignorerer eventuelle fejl. Da vi ikke kan sige noget om, hvilken kontekst Delete() vil blive kaldt i, er det ikke rimeligt, at tage beslutninger på den kaldende kodes vegne. Vi ved med andre ord ikke, om koden har brug for at skelne mellem succes og fejl.

Hvis koden kalder Delete(), må vi antage, at det er fordi applikationens tilstand af en eller anden grund får det til at give mening. Koden antager altså, at det kan lade sig gøre. Ved at ignorere en eventuel fejlsituation, fratager vi den kaldende kode muligheden for at opdage, at denne antagelse ikke holdt stik. Vi risikerer altså at skjule en fejl i koden.

Mark fulgte desuden op med følgende spørgsmål: Gør det nogen forskel om Delete() kaldes i et miljø med flere tråde? Nej, ikke i min optik.

File.Delete() er i høj grad underlagt denne problemstilling. Uanset at vi kan undersøge, om en given fil eksisterer lige inden, vi kalder Delete(), er denne operation altid underlagt en race condition. Filen kan forsvinde mellem vores kald til Exists() og Delete(), og den eneste måde, vi kan opdage, om dette er tilfældet, er, hvis Delete(), kaster en exception. Derefter må det være op til den kaldende kode, om den vil reagere på dette eller ej. Ved at undlade at reportere fejlen, fratager vi den kaldende kode muligheden for at reagere.

Derfor synes jeg, en exception er det rette valg i denne situation.

Kodekommentarer er ofte spild af tid

February 10th, 2010

Jeg har ikke tal på hvor mange gange, jeg har hørt udviklere efterlyse flere kommentarer i koden, men uden at have lavet en egentlig undersøgelse, er min fornemmelse, at jeg har set langt flere dårlige end gode kommentarer i min tid som udvikler. Derfor er det ikke just nærliggende for mig at efterspørge flere kommentarer. Nærmest tværtimod.

Lad mig dog slå fast, at jeg bestemt anerkender værdien af kommentarer i de tilfælde, hvor de faktisk tilfører koden værdi. I alle de andre tilfælde gør de ofte mere skade end gavn. Lad os se lidt på, hvad der adskiller de brugbare kommentarer fra de ubrugelige.

Eksempler

Problemerne med kommentarer er mange, så lad os se på nogle af dem. Min liste er næppe udtømmende (lad os endelig få flere skrækeksempler), men blot et udsnit af de brugsmønstre, vi sikkert alle er stødt på fra tid til anden. Det første eksempel er banalt og synes ikke at gøre den store skade:


// reset counter
cnt = 0;

Men lad os ikke lægge ud med at tale om skade. Lad os i stedet spørge: Hvilken værdi tilfører denne kommentar koden? Ingen. Den fortæller mig godt nok, at udvikleren har været for doven eller måske ikke har evnet at omdøbe sin variabel til Counter eller noget mere sigende. At forklare at værdien bliver nulstillet, når den sættes til nul, er en lige så overflødig oplysning som at tvillinger kommer i par. Kommentaren giver os altså ikke mere information end vi snildt kunne have udtrykt i selve koden, så undgå gentagelsen. DRY gælder også for kommentarer.

Det største problem ved denne slags kommentarer er desværre, at de ofte kommer i hobetal. Hvis udvikleren af en eller anden grund tror, at de faktisk er til gavn, bliver den slags ligegyldigheder strøet ud over det hele. Det gør koden længere uden at gøre noget som helst positivt for læsbarheden. Det er spild af alles tid.

En afart af ovenstående er, når kommentarer bruges til at forklare værdier. Det kan være ved tildeling som ovenfor eller ved kald af metoder. Eksempel:


// payout occurs every 3 weeks
Payout(3);

Brug dog en konstant i stedet for en kommentar, så er der tilmed chance for, at den kan bruges igen.

Lærebog i C#

Lad være med at skrive kommentarer, der forklarer, hvad forskellige sprogkonstruktioner gør. Antag at læseren kender sproget. Skulle det ikke være tilfældet, er kommentarer alligevel ikke løsningen på det problem.

Beskrivelser af forskellige sprogkonstruktioner findes allerede en masse, så lad være med at forsøge at gøre koden til en referencemanual.

// TODO

Nogen vil måske mene, at vi her bevæger os ind i grænselandet mellem det ubrugelige og det brugbare, for hensigten med TODO-kommentarerne er naturligvis at gøre opmærksom på, at der er noget, der ikke er fulgt helt til dørs. Det er reelt nok, og jeg anvender selv TODO i min kode, mens jeg arbejder, men det er ikke i orden at checke kode ind med TODO-kommentarer.

Med mindre I bruger jeres repository til at planlægge arbejdsopgaver, er det jo et fjollet sted at registrere nye opgaver. Opret et work item, en incident, en change request eller hvad I nu kalder den slags og sørg for at få beskrevet, hvad der mangler, så I har en chance for at få fulgt op på det. Kildetekst er ikke særlig brugbar som projektstyringsværktøj.

Mange TODO-kommentarer er tilmed frygtelig indforståede. Jeg faldt over nedenstående under et code review for et stykke tid siden:


// TODO major hack!!!

Hvad forventes læseren at tænke her? Er nedenstående et major hack? I så fald hvordan og hvorfor? Mangler der er major hack her? Til hvad? Er der noget, der ikke virker? Det er bare ikke acceptabelt at gøre den slags.

Kommentarer som overskrifter

En hyppig anvendt fremgangsmåde er at bruge kommentarer som overskrifter for dele af koden. Udviklere, der anvender denne form for kommentarer, har ofte metoder, der er lange, men hvor kommentarerne formodes at give det gyldne overblik.

Således finder vi som regel kommentarer i stil med ”her initialiseres nødvendige strukturer”, ”her udregnes”, ”her udskrives”, ”her gemmes data” og så videre. Bagtanken er naturligvis, at læseren skal kunne tilegne sig et overblik ved at kigge ned over koden. Ideen er sympatisk men ikke særlig veludført.

Hvis en stump kode gør noget, vi kan sætte en fornuftig overskrift på, er det så ikke nærliggende, at denne funktionalitet eventuelt kan genbruges? Det gør kommentarer ikke det mindste for at fremme.

Et andet, større problem er, at ideen antager, at vi altid ser på koden i sin nuværende form, men hvad med de situationer, hvor vi virkelig har brug for at finde ud af, hvad der sker som f.eks. under fejlsøgning? Får vi en exception, er de velmenende kommentarer jo desværre ikke en del af vores stack trace. Her vil navnet på en kæmpe metode og et offset være ulig meget mindre værd end navnet på en lille, specifik undermetode.

I begge tilfælde er det meget mere anvendelig at skifte overskrifterne ud med metodekald. Måske endda metodekald på nye typer for derved at undgå, at hver type laver mere end en ting. Lav de enkelte blokke som metoder. Det giver korte metoder, der er lette at overskue og en god hierarkisk opdeling af opgaverne. Visual Studio gør det tilmed let at opdele metoder på denne måde via Extract Method. Ja, det giver flere metoder og flere kald, men kode, der er let at læse og vedligeholde, er altså uendelig meget mere værd end kompliceret kode, der måske kører en mikroskopisk brøkdel hurtigere.

De få tilfælde

Hvornår er det så okay at bruge kommentarer? Det korte svar er: ikke særlig ofte. Hver gang vi skal til at skrive en kommentar, bør vi i hvert fald lige tænke over, om der ikke er en bedre måde at udtrykke os i koden. Hvis kommentaren forsøger at lappe et hul, så lap hullet i stedet for at beskrive det. Hvis kommentaren forsøger at forklare kompleks kode, så skriv koden om, så den bliver forståelig. Hvis kommentaren forklarer en rodet struktur, så ryd op.

I mine øjne er kommentarer i kildeteksten kun brugbare, når de fortæller læseren noget, der ikke umiddelbart kan læses ud af koden. Forskellen på god og dårlig kode kendes som bekendt på antallet af WTF?!-udbrud hos læseren. Hvis læseren blot sidder og nikker og tænker, ”ja ja ja”, ”det giver mening”, ”sådan ville jeg også have lavet det”, er vi på rette spor.

Er vi tvunget væk fra dette spor, kan kommentarer være en hjælp til læseren. Det kan f.eks. være i tilfælde, hvor vi er nødt til at vælge en ikke oplagt løsning af hensyn til performance eller på grund af fejl, vi ikke har mulighed for at udbedre. Forklar læseren hvorfor det uventede giver mening i netop denne situation.

Klassebiblioteker

Et andet område, hvor kommentarer kan være anvendelige, er ved beskrivelse af offentlige typer og metoder i et klassebibliotek.
Det er fint med kommentarer, der forklarer intentionerne med og brugen af en type eller et interface (altså hvad og ikke hvordan), men det giver ikke rigtig nogen merværdi at få at vide, at et parameter ved navn filename indikerer navnet på en fil. Hvis læseren ikke kan læse navnet, er der ingen grund til at tro, at han kan læse kommentaren. Brug i stedet kræfterne på at vælge gode navne til metoder og argumenter og dokumenter så ideer og facetter, der ikke let kan læses ud af koden.

Der er ingen grund til at dokumentere alt blot for at kunne sige, at vi har dokumenteret. Kommentarer koster også tid og ressourcer at vedligeholde, så der er ingen grund til at skrive flere af dem end nødvendig. Ligesom kode kan kommentarer indeholde fejl, og fejlagtige kommentarer er som regel værre end ingen kommentarer. Hvis vi skriver få, meningsfyldte kommentarer, er det lettere at vedligeholde koden, end hvis vi på mekanisk vis forsøger at kommentere alt i detaljer.