11/04/2013

Codility

Home

Jestem wielkim zwolennikiem sprawdzania kandydatów na programistów przy pomocy zadań wymagających napisania kodu. Sam również byłem egzaminowany w ten sposób nie raz i nie dwa. W pamięci zapadły mi jednak rekrutacje z udziałem portalu Codility, który weryfikuje nie tylko poprawność kodu ale również jego wydajność, za każdym razem było to dla mnie ciekawe wyzwanie.

Postanowiłem więc skontaktować się z Codility i zapytać czy w ofercie mają produkt pozwalający programistom ćwiczyć ich umiejętności. Odpowiedź na zapytanie dostałem bardzo szybko i niestety okazała się negatywna, ale zostałem zaproszony do ich biura w Warszawie aby porozmawiać o tym pomyśle.

Trochę to trwało zanim udało się nam ustalić termin spotkania, ale w końcu pewnego popołudnia wsiadłem w tramwaj i pojechałem na Plac Bankowy. Na miejscu przywitała mnie przemiła Czeszka Zuzana, miałem okazję poznać zespół pracujący nad rozwojem Codility oraz porozmawiać o ich pracy. Ponieważ nie miałem wcześniej okazji korzystać z portalu od strony rekrutera pokazano mi jak to wygląda.

Na koniec wręczono mi upominek w postaci książki Looking For a Challenge? z opisem kilkudziesięciu ciekawych zadań programistycznych, przygotowanych przez zwycięzców międzynarodowych konkursów programistycznych.

À propos problemów algorytmicznych, dowiedziałem się również, że część zadań Codility dostępna jest w Internecie dla każdego programisty, ale nie wszystkie łatwo znaleźć. Poniżej, dzięki uprzejmości Codility, macie ich pełną listę. Lista ta z czasem będzie z czasem rozszerzana o tzw. zadania well known czyli takie, które są dobrze znane i nie ma sensu przy ich pomocy testować kandydatów ale idealnie nadają się do ćwiczeń.
Wizytę wspominam bardzo miło. Tym bardziej, że Codility odwiedziłem nie jako klient, ale jako "człowiek z ulicy". Cieszy również, że to polski start-up odnoszący sukcesy na świecie.

23/03/2013

Migracja bazy danych w EF

Home

Już dawno temu pisałem o zastosowaniu RavenDB w swoim projekcie. Początkowo byłem zadowolony z tego wyboru ale jakiś czas temu zdecydowałem się przejść na Entity Framework + SQLCe. Czemu? To temat ta cały post, tak w skrócie to zirytowało mnie to, że RavenDB stara się być mądrzejszy od programisty oraz to jak trudno osiągnąć w nim niektóre rzeczy. Nie twierdzę, że RavenDB jest zły, ale tak jak inne dokumentowe bazy danych nie do wszystkiego się nadaje.

Wracając do tematu, przechodząc do EF zdecydowałem się wykorzystać podejście code first. Wcześniej tego nie próbowałem i muszę powiedzieć, że byłem bardzo zadowolony, migracja odbyła się w miarę bezboleśnie. Swoją drogą zaprocentowała początkowa decyzja aby ukryć technologię dostępu do danych za dobrze określonym interfejsem. Dzięki temu zmiany musiałem wprowadzić w niewielu miejscach. Nie ma jednak róży bez końców. Kiedy po jakimś czasie chciałem dodać nową właściwość do swoich encji otrzymywałem błąd:

The model backing the context has changed since the database was created...

Za pierwszym razem poradziłem sobie z tym tworząc bazę danych od początku i importując dane ale na dłuższą metę byłoby to uciążliwe. Zacząłem kombinować nad rozwiązaniem, które zmieniłoby schemat bazy danych przed użyciem EF. Wtedy natknąłem się na artykuł opisujący to zagadnienie i był to strzał w dziesiątkę. Oto krótki opis co zrobiłem:.
  • Dodałem nową właściwość do encji.
  • Zmodyfikowałem logikę biznesową.
  • Uruchomiłem Package Manager Console.
  • Jako Default project wybrałem projekt, z klasę dziedziczącą po DbContext. Jeśli takich klas jest więcej zostanie zgłoszony błąd.
  • Wydałem polecenie Enable-Migrations.
  • Do projektu został dodany katalog Migrations, a w nim klasa Configuration.
  • Wydałem polecenie Add-Migration tj. Add-Migration AddColumn.
  • W projekcie została utworzona klasa AddColumn dziedzicząca po DbMigration. Dla mnie interesujące była metoda Up, w której umieściłem kod odpowiedzialny za uaktualnienie bazy danych. Wyglądało to w następujący sposób:

    public override void Up()
    {
       AddColumn("TranslationEntities", "Translation2", c => c.String(true, maxLength: 4000, storeType: "nvarchar"));
    }
    

    Metoda AddColumn to metoda klasy DbMigration. Jest ich dużo więcej np.: AddForeignKey, CreateIndex czy RenameColumn.
  • Na koniec użyłem klasy MigrateDatabaseToLatestVersion w celu zainicjowania bazy danych:

    Database.SetInitializer(new MigrateDatabaseToLatestVersion<ExpressionsDataContext, Configuration>());

  • Przy kolejnym uruchomieniu aplikacji zadziały się dwie rzeczy. Do tabelki TranslationEntities została dodana nowa kolumna oraz utworzona została nowa tabela __MigrationHistory. Dzięki tej nowej tabelce EF będzie śledził historię wprowadzanych zmian i nie uruchomi tej samej migracji więcej niż raz.
To bardzo prosty przykład. Jestem ciekawy jak ta funkcjonalność spisuje się w bardziej skomplikowanych scenariuszach. Czy macie jakieś doświadczenia?

22/03/2013

Konwencja wołania

Home

Konwencja wołania (ang. Calling convention) to zestaw zasad, który określa w jaki sposób metoda wołająca przekazuje parametry do metody wołanej i odbiera od niej wyniki. Programując w .NET, o ile nie współpracujemy z kodem natywnym, w ogóle nie musimy się tym interesować. Zgłębiając ten temat można jednak nauczyć się kilku ciekawych rzeczy.

Zacznijmy od tego jakiej konwencji wołania używa CLR. Na to pytanie można odpowiedzieć na dwa sposoby. Odpowiedź "wysoko poziomowa" brzmi: CLR został zaimplemntowany jako maszyna stosowa i nie używa rejestrów, a więc parametry oraz wyniki zostaną przekazane na stosie. Oto prosty przykład, zacznijmy od klasy:

public class Test
{
        public string ReturnString(int i, bool b)
        {
            return i.ToString() + b.ToString();
        }

        public string ReturnString(Fun f)
        {
            return f.ToString();
        }
}

Teraz spójrzmy na kod w C# i odpowiadający mu kod IL, w którym użyto tych metod:

var t = new Test();

newobj instance void ConsoleApplication1.Test::.ctor()
stloc.0 

var res1 = t.ReturnString(10, true);

ldloc.0 
ldc.i4.s 10
ldc.i4.1 
callvirt instance string ConsoleApplication1.Test::ReturnString(int32, bool)
stloc.1 

var res2 = t.ReturnString(new Fun());

ldloc.0
newobj instance void ConsoleApplication1.Fun::.ctor()
callvirt instance string ConsoleApplication1.Test::ReturnString(class ConsoleApplication1.Fun)
stloc.2 

Istotne fragmenty zaznaczyłem na czerwono. ldloc.0 odpowiada za wrzucenie na stos obiektu, na rzecz którego zostanie wywołana metoda (this). Potem wrzucamy argumenty dla wołanej metody czyli ldc.i4.s 10 oraz ldc.i4.1. Po wywołaniu metody zdejmujemy natomiast wynik ze stosu i zapisujemy w zmiennej lokalnej o indeksie 1 przy pomocy komendy stloc.1. Dla drugiego wywołania wygląda to podobnie. Polecenie newobj tworzy nowy obiekt i umieszcza referencję do niego na stosie.

Czemu jednak CLR został zaimplementowany jako automat stosowy (maszyna wirtualna Java zresztą też), a rejestry pojawiają się dopiero kiedy program jest wykonywany i IL jest zamieniany na kod natywny? Otóż CLR to komercyjna implementacja VES (ang. Virtual Execution System). VES jest częścią standardu ECMA-335, który jest niezależny od docelowej architektury, a w szczególności nie wspomina nic o żadnych rejestrach. Dzięki temu konkretne implementacji standardu mogą używać rejestrów w dowolny sposób.

I tutaj dochodzimy do odpowiedzi "nisko poziomowej" na postawione pytanie, czyli jakiej konwencji używa kod natywny wygenerowany na podstawie IL dla Windows'a. Otóż jest to konwencja fastcall, w której na przykład dla architektury x86 dwa pierwsze parametry przekazywane są w rejestrach ECX oraz EDX. Więcej szczegółów na ten temat można znaleźć na blogu Joe Duffy'ego.

Od siebie dodam, że jeśli korzystamy z P/Invoke to domyślnie metody natywne wołane są przy pomocy konwencji Winapi, która w systemie Windows jest tożsama z Stdcall. Zachowanie to możemy zmienić przy pomocy właściwości CallingConvention atrybutu DllImportAttribute.

25/02/2013

Coursera

Home

Ponad rok temu pisałem o możliwości wzięcia udziału w kursach online dotyczących uczenia maszynowego czy baz danych prowadzonych przez Uniwersytet Stanforda. Kursy te okazały się strzałem w dziesiątkę i na przykład na ten dotyczący uczenia maszynowego zapisało się około 100 tysięcy osób!

Wracam do tego tematu ponieważ ostatnio kolega wspomniał mi o projekcie/firmie Coursera, która oferuje każdemu, bezpłatnie, możliwość wzięcia udziału w ponad 300 kursach online, z różnych dziedzin, które zostały przygotowane przez uczelnie z całego świata. Interesują cię kompilatory? A może gra na gitarze lub system monetarny Chin? Każdy powinien znaleźć coś dla siebie. Projekt dopinguję tym bardziej, że założycieli jest profesor nadzwyczajny z Uniwersytetu Stanforda Andrew Ng, który przygotował i poprowadził wspomniany kurs uczenia maszynowego.

Na bieżącą chwilę Coursera ma już 2 mln 700 tysięcy użytkowników. Nieźle jak na projekt, który wystartował niecały rok temu. W każdym razie uważam, że to wspaniała inicjatywa, tym bardziej warta docenienia, że daje dostęp do rzetelnej wiedzy za darmo, co w obecnych czasach jest coraz mniej oczywiste. Żeby tylko znaleźć na to czas ;)

16/02/2013

Co to jest dobre oprogramowanie?

Home

Pytanie jak powyżej pojawiło się w rozmowie w jakiej ostatnio uczestniczyłem. Każdy z nas ma na pewno swoje zdanie na temat cech, które powinien posiadać dobry program/system. Nie inaczej było w tym przypadku i padło wiele odpowiedzi. Po bliższym przyjrzeniu się, okazało się jednak, że odpowiedzi te można pogrupować w raptem cztery kategorie, co było dla mnie pewnym zaskoczeniem. Dodam, że w rozmowie uczestniczyło pięć osób, wszystkie techniczne, ale o różnym doświadczeniu, które pracowały przy różnych projektach i w różnych firmach. Te cztery kategorie to począwszy od najpopularniejszej:
  • Program/system powinien być łatwy oraz tani w utrzymaniu i rozwoju. Do tej kategorii można wrzucić bardzo wiele rzeczy: odpowiednia architektura, obecność testów jednostkowych, dobra dokumentacja wymagań, stosowanie wzorców projektowych itd. Pojawienie się tej kategorii jednoznacznie wskazuje na programistyczny background osób biorących udział w rozmowie.
  • Program/system powinien spełniać wymagania i oczekiwania klienta, rozwiązywać jego problemy.
  • Program/system powinien spełniać wymagania i oczekiwania użytkownika, być ergonomiczny i łatwy w użyciu. Kategoria ta może wydawać się podobna do drugiej kategorii, ale kładzie nacisk na użytkownika końcowego, który w wielu przypadkach nie jest tożsamy z zamawiającym oprogramowanie (klientem).
  • Program/system powinien przynosić firmie tworzącej oprogramowanie zyski, dobrze się sprzedawać. Bez tego też nie da się obejść. Nawet jeśli oprogramowanie zostało napisane z zachowaniem wszystkich zasad i jest cudeńkiem inżynierii, ale się nie sprzedaje, to nie można nazwać go dobrym.
Sądzę, że zestawienie to, pomimo, że krótkie, jest kompletne. Udało się w nim uchwycić interesy różnych grup ludzi, którzy uczestniczą w przedsięwzięciu informatycznym czyli: firmy informatycznej, zespołu tworzącego produkt, klienta/firmy zamawiającej oraz użytkowników końcowych. Interesy tych czterech grup są nierzadko ze sobą sprzeczne ale dopiero kompromis pomiędzy nimi pozwala stworzyć naprawdę dobry system informatyczny.

Po zakończeniu tego posta zacząłem się zastanawiać ile projektów, o których słyszałem, rozmawiałem lub przy których pracowałem spełnia powyższe wymagania. W większości przypadków, które przyszły mi do głowy, mogę gdzieś wsadzić szpilkę. A to system co prawda przynosi kupę kasy ale jest napisany w taki sposób, że woła o pomstę do nieba... Inny z kolei to kawał dobrej programistycznej roboty, ale z jego urynkowieniem jest już gorzej... O wiele łatwiej jest mi znaleźć oprogramowanie wystarczająco dobre, czyli spełniające tylko niektóre z tych wymagań.

A jakie są Wasze doświadczenia?

05/02/2013

WolframAlpha

Home

WolframAlpha to ambitny projekt stworzenia silnika, który umiałby odpowiadać na pytania wyrażone w języku naturalnym, a co więcej odpowiedzią nie byłaby sucha lista stron w Internecie, ale zbiór faktów stanowiących odpowiedź na pytanie. O tym przedsięwzięciu wiedziałem już od dłuższego czasu, ale traktowałem je raczej jako nowinkę i go nie używałem.

Ostatnio kolega zwrócił mi jednak uwagę na możliwość rozwiązywania równań przy pomocy tego silnika. Od tej pory przestałem traktować go tylko jako ciekawostkę i jest pod ogromnym wrażeniem tego projektu, zastosowanych w nim algorytmów, no i oczywiście umiejętności programistów.

Może mały przykład. Jakiś czas temu inny kolega wysłał mail'a z zaproszeniem do poczęstunku z okazji urodzin, ale swój wiek podał w taki oto sposób:



Prawa część równania nie stworzy chyba nikomu problemów. Gorzej jednak z tą całką i sumą szeregu matematycznego, chociaż wyglądają znajomo. Kiedy byłem świeżo po kursie analizy matematycznej pewnie obudzony w środku nocy, po libacji alkoholowej, podałbym wynik bez zastanowienia ;) Teraz jednak, zamiast przypominać sobie wzory, wklepałem to równanie do WolframAlpha:

sum x = 1 to infinity 1/x^2)/(integral from -1 to 1 (1-x^2)^0.5)^2*(2^2^3 - 2^2^2)/2^2

i w mgnieniu oka uzyskałem wynik. Silnik potrafi również rysować wykresy, rozpoznaje rodzaje funkcji np.: powie, że zadane równanie to paraboloida, liczy pochodne, wyznacza nie tylko wartość całki oznaczonej, ale również potrafi policzyć całkę nieoznaczoną, rozwiązuje równania różniczkowe oraz robi pewnie setki innych rzeczy, o których jeszcze nie mam pojęcia. W każdym razie WolframAlpha na stałe zagości w moim przyborniku.

16/01/2013

Mono C# compiler as a service 3

Home

Ponownie wrócę do tematyki kompilowania C# w locie. Pisałem wcześniej, że potrzebowałem takiej funkcji aby użytkownicy mojej aplikacji mogli w dowolnym momencie zdefiniować własny algorytm obliczania odległości między dwoma wektorami.

Po kilku próbach już wiedziałem jak to zrobić, a chwilę później miałem już zaimplementowaną pierwszą wersję rozwiązania. Przyszła pora wypróbowania nowej zabawki na prawdziwych danych. Uruchomiłem więc aplikację, napisałem krótki skrypt i wystartowałem obliczenia. W tym momencie aplikacja zamarła i było widać, że pożera wszelkie dostępne zasoby. Spodziewałem się czegoś innego :). Skończyło się zabiciem aplikacji. Co zrobiłem nie tak?

Dane jakich użyłem zawierały kilkaset tysięcy wektorów. Oznacza to, że odległość między wektorami musiała zostać obliczona "dużoooooooooooooooooooooo" razy. Każda taka operacja powodowała wywołanie omówionej już metody Evaluator.Run, czyli za każdym razem ten skrypt był ponownie kompilowany, a to trochę trwa. Ziarnko do ziarnka, a zbierze się miarka jak mówi przysłowie. Co więcej każde wywołanie Evaluator.Run powoduje załadowanie do pamięci kolejnego dynamicznie utworzonego modułu. Moduły te są potem usuwane ale to też trwa. W ramach eksperymentu proponuję uruchomić pod kontrolą debugger'a taki kod:

var settings = new CompilerSettings();
var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter()));

for (int i = 0; i < 1000; ++i)
 ev.Run("System.Console.WriteLine(\"Hello!\");");

Zamiast zobaczyć, że kod wykonuje się błyskawicznie, zaobserwujemy jak VS próbuje załadować symbole dla kolejnych modułów (komunikat w rodzaju Loading symbols for eval-120... na pasku stanu). Jeśli postawimy pułapkę wewnątrz pętli i otworzymy okno Modules to na liście znajdziemy np.: eval-100, eval-101, eval-102...

W takiej sytuacji skrypt należy skompilować raz, a potem wielokrotnie używać wyniku kompilacji. Służy do tego metoday Evaluator.Compile, która zwraca instancję klasy CompiledMethod. Jeśli chcemy wywołać tak przygotowany skrypt korzystamy z metody CompiledMethod.Invoke. Nic trudnego. Parametry do takiego skompilowanego skryptu można przekazać w dokładnie taki sam sposób jak opisałem w poprzednim poście. Po zmianach powyższy kod będzie wyglądał jak poniżej i wykona się błyskawicznie:

var settings = new CompilerSettings();
var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter()));

var method = ev.Compile("System.Console.WriteLine(\"Hello!\");");

object o = null;
for (int i = 0; i < 1000; ++i)
 method.Invoke(ref o);

Na koniec dodam, że po zastosowaniu takiego zabiegu nie widzę różnicy w czasie odpowiedzi pomiędzy przypadkiem kiedy używam algorytmu osadzonego/wkompilowanego w aplikację, a algorytmu dostarczonego przez użytkownika.

13/01/2013

Mono C# compiler as a service 2

Home

Dzisiaj wrócę do tematu użycia języka C# jako języka skryptowego przy pomocy Mono.CSharp.dll i opiszę w jaki sposób przekazać parametry do takiego skryptu. Pominę podejście opierające się o wklejanie do skryptu string'owej reprezentacji takich parametrów i od razu przejdę do bardziej eleganckiego rozwiązania. Bazuje ono na tym co przeczytałem w tym poście.

Zaczynamy od utworzenia statycznej klasy ScriptContext, która posłuży nam do wymiany danych pomiędzy skryptem, a naszym programem.

namespace Scripting
{
    public static class ScriptContext
    {
        static ScriptContext()
        {
            Items = new Dictionary<string, object>();
        }

        public static object Result { get; set; }

        public static IDictionary<string, object> Items { get; set; }
    }
}

Użycie jej nie jest trudne. Mały przykład:

var settings = new CompilerSettings();
var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter()));
            
//Informujemy silnik skryptowy gdzie została zdefiniowana klasa ScriptContext
ev.ReferenceAssembly(typeof(ScriptContext).Assembly);

//Ustawiamy parametry
ScriptContext.Items["param1"] = new List<string> { "Hello!", "Welcome!", "Hi!" };

ev.Run("using System;");
ev.Run("using Scripting;");
//Korzystamy z przekazanych parametrów
ev.Run("foreach(var s in (System.Collections.Generic.IEnumerable<string>)ScriptContext.Items[\"param1\"]) Console.WriteLine(s);");

Właściwość ScriptContext.Result dodałem aby nie musieć zastanawiać się kiedy wywołać metodę Evaluator.Run, a kiedy Evaluator.Evaluate. Zawsze używam tej pierwszej, zakładając, że wynik ze skryptu zwracany jest przy użyciu tej właściwości np.:

ev.Run("ScriptContext.Result = 1;");
Console.WriteLine(ScriptContext.Result);

Do szczęścia brakowało mi jednak jeszcze jednej rzeczy. Jak pokazuje pierwszy przykład musiałem użyć rzutowania do IEnumerable<string> aby cieszyć się silnie typowanym parametrem. Napisałem więc metody pomocnicze, które przygotowują silnie typowane parametry:

public static class ScriptingHelper
{
 public static StringBuilder PrepareParameters(IDictionary<string, object> parameters)
 {
  ScriptContext.Items = parameters;

  var sb = new StringBuilder();

  if (parameters != null)
  {
   foreach (var kvp in parameters)
   {
    var typeName = ExtractTypeName(kvp);

    sb.AppendFormat("var {0} = ({1}){2}.Items[\"{0}\"];", kvp.Key, typeName, typeof(ScriptContext).Name);
    sb.AppendLine();
   }
  }

  return sb;
 }

 private static string ExtractTypeName(KeyValuePair<string, object> kvp)
 {
  if (kvp.Value == null)
   throw new Exception("null parameters are not supported!");
   
  var type = kvp.Value.GetType();

  using (var provider = new CSharpCodeProvider())
  {
   var typeRef = new CodeTypeReference(type);
   return  provider.GetTypeOutput(typeRef);
  }
 }
}

Nic bardzo skomplikowanego ale warto zwrócić uwagę na użycie klasy CSharpCodeProvider. Dzięki niej uzyskuję poprawną, pełną nazwę typów, również tych generycznych. Gdybym na przykład dla listy int'ów spróbował użyć czegoś w rodzaju list.GetType().Name to w wyniku otrzymałbym List`1, co do rzutowania się nie nadaje. Wcześniejszy przykład można więc napisać teraz w taki sposób:

//Ustawiamy parametry
var parameters = ScriptingHelper.PrepareParameters(new Dictionary<string, object>
{
 {"param1", new <string> { "Hello!", "Welcome!", "Hi!" } }
});

ev.Run("using System;");
ev.Run("using Scripting;");
ev.Run(parameters.ToString());
//Korzystamy z przekazanych parametrów ale już bez rzutowania
ev.Run("foreach(var s in param1) Console.WriteLine(s);");

Wracając do mojej aplikacji, w której chciałem umożliwić użytkownikowi definiowanie własnych algorytmów obliczania odległości między wektorami. Dane wejściowe to oczywiście dwa wektory. Dzięki takiemu podejściu, zamiast zmuszać użytkownika to rzutowania parametrów na określony typ, używam konwencji czyli: wektor numer 1 znajduje się w zmiennej vector1 itd.

Klasa ScriptingHelper jest również doskonałym miejsce na dodawanie różnych innych "przydasiów" ułatwiających pracę użytkownikowi.

12/01/2013

Mono C# compiler as a service

Home

Od jakiegoś czasu pracuję nad aplikacją do generowania i analizowania wykresów rekurencyjnych. Temat sam w sobie jest bardzo ciekawy, więc może do niego wrócę w przyszłości, ale dzisiejszy post będzie o czymś innym.

Moja aplikacja między innymi wykonuje obliczenia na wektorach np.: oblicza różne odległości (euklidesową, Manhattan czy normę maksimum) między nimi. Dodawanie kolejnych algorytmów wymagało jednak każdorazowej rekompilacji aplikacji. Zacząłem więc szukać sposobu aby umożliwić użytkownikowi dodawanie własnych algorytmów w sposób dynamiczny. Innymi słowy potrzebowałem jakiegoś silnika skryptowego.

Początkowo pomyślałem o PowerShell'u, rozważałem również IronPython'a. Trafiłem jednak na krótki i treściwy post na temat biblioteki Mono.CSharp.dll, która stanowi część dobrze znanego projektu Mono. Opis wyglądał obiecująco dlatego postanowiłem wypróbować tą bibliotekę i był to strzał w dziesiątkę.

Jądrem biblioteki jest klasa Mono.CSharp.Evaluator, która służy do dynamicznego kompilowania i wykonywania kodu C#. Jej użycie jest wręcz trywialne. Oto prosty przykład Hello World!:

var settings = new CompilerSettings();
var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter()));

ev.Run("using System;");
ev.Run("Console.WriteLine(\"Hello World!\");");

Użycie ConsoleReportPrinter powoduje, że wszelkie komunikaty kompilatora zostaną wypisane na ekran konsoli. Możemy też użyć klasy StreamReportPrinter, wtedy komunikaty zostaną zapisane do wskazanego strumienia. Klasa CompilerSettings pozwala natomiast skonfigurować różne aspekty pracy kompilatora. Kolejny prosty przykład pokazuje, jak zwrócić wynik ze skryptu:

ev.Run("Console.Write(\">\")");
ev.Run("var s = Console.ReadLine();");
var s = (string)ev.Evaluate("s;");

Ciekawe jest to, że możemy zmienić typ raz zadeklarowanej zmiennej np.:

ev.Run("int s = 1;");
ev.Run("Console.WriteLine(s);");
ev.Run("string s = \"Hello!\";");
ev.Run("Console.WriteLine(s);");
ev.Run("bool s = true;");
ev.Run("Console.WriteLine(s);");

Możliwe jest również zdefiniowanie klasy:

ev.Run("class Test { public string Fun(int i) { return \"Hello \" + i; } }");
ev.Run("Console.WriteLine(new Test().Fun(1));");

Załóżmy, że w oddzielnym projekcie mamy zdefiniowaną klasę Test2. Poniższy przykład pokazuje jak użyć jej w skrypcie:

ev.ReferenceAssembly(typeof(Test2).Assembly);
ev.Run("using TestLib;");
ev.Run("Console.WriteLine(new Test2().Welcome(\"Kate\"));");

Praca z klasą Mono.CSharp.Evaluator bardzo mi się podoba. To kawał dobrej roboty, zachęcam więc do wypróbowania. W kolejnych postach opiszę, jak w elegancki sposób przekazać argumenty do skryptu oraz kiedy warto skorzystać z metody Compile zamiast Run lub Evaluate.

Uwaga instalacyjna

Pisząc ten post korzystałem z wersji beta 3.0.3 Mono. Ostatnia dostępna stabilna wersja 2.10.9 zawiera bowiem błąd, który objawia się komunikatem:

Method 'Mono.CSharp.Location.ToString()' is security transparent, but is a member of a security critical type.

przy pierwszej próbie użycia klasy Evaluator.  Nie sprawdzałem ale możliwe, że z github'a można ściągnąć wersję bez tego błędu.

24/12/2012

Jeszcze o radzeniu sobie z głęboką rekursją

Home

W poście opisałem ogólne podejście to radzenia sobie z bardzo głęboką rekursją, która prowadzi do StackOverflowException. Dzisiaj powrócę do tematu. Od czasu do czasu, oprócz pogłębiania wiedzy na temat bibliotek, framework'ów itd. lubię rozwiązywać różnego rodzaju algorytmiczne zadania programistyczne. Jakiś czas temu rozwiązywałem takie zadanie:

Zadana jest mapa świata w postaci prostokątnej tablicy dwuwymiarowej. Każdy element tablicy ma swój kolor. Sąsiadujące (stykające się jednym z boków) z sobą elementy tego samego koloru należą do jednego kraju. Należy policzyć liczbę krajów na tej mapie. Jeśli dwa elementy tablicy mają ten sam kolor, ale nie znajdują się w jednym ciągłym obszarze, to należą do różnych krajów. 

Poniższy rysunek przedstawia przykładową mapę takiego świata, na który składa się 6 krajów.



Zacznijmy od głównej metody wykonującej obliczenia. Koncepcja jest prosta. Nie ulega wątpliwości, że należy odwiedzić wszystkie elementy tablicy aby móc zwrócić poprawny wynik. Aby nie zliczać tego samego elementu dwa razy używam wartości Int32.MaxValue jako znacznika już odwiedzonych elementów. Jeśli odwiedzając kolejny element znajduję wartość inną niż Int32.MaxValue to znaczy, że danego elementu jeszcze nie odwiedziłem i zwiększam licznik krajów.

public int Count(int[][] map)
{
 if (map == null) return 0;
 
 if (map.Length == 0) return 0;

 var numberOfCountries = 0;

 for (var y = 0; y < map.Length; ++y)
 {
  for (var x = 0; x < map[0].Length; ++x)
  {
   if (map[y][x] != Int32.MaxValue)
   {
    numberOfCountries++;
    /* Zaznacz elementy należące do danego kraju */
    VisitCountry(map, map[y][x], y, x);
   }
  }
 }

 return numberOfCountries;
}

Co kryje się pod tajemniczym Zaznacz elementy należące do danego kraju? W pierwszym podejście napisałem funkcję rekurencyjną, która począwszy od zadanego elementu odwiedzała wszystkie pola należącego do danego kraju i oznaczała je jako odwiedzone.

public void VisitCountry(int[][] map, int currentColor, int y, int x)
{
 if (y < 0 || x < 0 || y >= map.Length || x >= map[0].Length) return;

 if (map[y][x] != currentColor) return;
            
 map[y][x] = Int32.MaxValue;

 VisitCountry(map, currentColor, y + 1, x);
 VisitCountry(map, currentColor, y, x + 1);
 VisitCountry(map, currentColor, y - 1, x);
 VisitCountry(map, currentColor, y, x - 1);
}

Poniżej kod testujący dla przykładu z początku postu:

int[][] map = new int[5][];

for (int i = 0; i < map.Length; ++i)
 map[i] = new int[4];

map[0][0] = 1; map[0][1] = 3; map[0][2] = 3; map[0][3] = 2;
map[1][0] = 1; map[1][1] = 2; map[1][2] = 3; map[1][3] = 2;
map[2][0] = 1; map[2][1] = 1; map[2][2] = 1; map[2][3] = 1;
map[3][0] = 2; map[3][1] = 3; map[3][2] = 3; map[3][3] = 1;
map[4][0] = 2; map[4][1] = 2; map[4][2] = 2; map[4][3] = 2; 

var result = Count(map);

W warunkach zadania było jednak napisane, że szerokość/wysokość mapy może znajdować się w przedziale od 1 do 1000000. Sprawdźmy, więc czy kod ten obsłuż dużo większą mapę:

int[][] bigMap = new int[10000][];
for (int i = 0; i < bigMap.Length; ++i)
{
 bigMap[i] = new int[10000];
 for (int j = 0; j < bigMap.Length; ++j)
  bigMap[i][j] = i;
}

var result = Count(bigMap);

Na mojej maszynie wystarczy już mapa 10000 x 10000 aby pojawił sie wyjątek StackOverflowException. Zgodnie z tym co napisałem we wcześniejszym poście metoda VisitCountry powinna więc wyglądać tak:

public void VisitCountry(int[][] map, int startY, int startX)
{
 var toCheck = new Queue<Tuple<int, int>>();
 toCheck.Enqueue(new Tuple<int, int>(startY, startX));

 var currentColor = map[startY][startX];

 while (toCheck.Count > 0)
 {
  var t = toCheck.Dequeue();

  if (t.Item1 < 0 || t.Item2 < 0 || t.Item1 >= map.Length || t.Item2 >= map[0].Length) continue;

  if (map[t.Item1][t.Item2] != currentColor) continue;

  map[t.Item1][t.Item2] = Int32.MaxValue;

  toCheck.Enqueue(new Tuple<int, int>(t.Item1 + 1, t.Item2));
  toCheck.Enqueue(new Tuple<int, int>(t.Item1, t.Item2 + 1));
  toCheck.Enqueue(new Tuple<int, int>(t.Item1 - 1, t.Item2));
  toCheck.Enqueue(new Tuple<int, int>(t.Item1, t.Item2 - 1));
 }
}

Tym razem mapa 10000 x 10000 i większe zostanie poprawnie obsłużona. Kod ten można jeszcze optymalizować. Na przykład zliczać ile pól się już odwiedziło i przerwać dalsze przetwarzanie, jeśli odwiedziło się wszystkie. Dalszą optymalizację kodu można potraktować jako ćwiczenie.

22/12/2012

Go

Home

GO to komenda, która sygnalizuje, że narzędzie takie jak sqlcmd powinno wysłać bieżący batch kodu T-SQL do instancji SQL Server'a. Do tej pory nie przywiązywałem do niej dużej uwagi, skupiając się na właściwym kodzie. Pominięcie GO może jednak doprowadzić do kłopotów. Popatrzmy na poniższy skrypt, który tworzy procedurę składowaną pr_Fun.

IF OBJECT_ID(N'dbo.pr_Fun') IS NOT NULL
BEGIN
      DROP PROCEDURE dbo.pr_Fun
END
GO

CREATE PROCEDURE dbo.pr_Fun
AS
BEGIN
      /*...*/
      RETURN
END

GRANT EXECUTE ON dbo.pr_Fun TO public
GO

Na pierwszy rzut oka wygląda prawidłowo i jeśli spróbujemy go uruchomić wykona się bez żadnych problemów. Niestety ale zawiera błąd, który pojawi się dopiero kiedy z procedury pr_Fun spróbuje skorzystać użytkownik z ograniczonymi uprawnieniami. Otrzyma taki komunikat:

Msg 229, Level 14, State 5, Procedure pr_Fun, Line 1
The EXECUTE permission was denied on the object 'pr_Fun', database 'Test', schema 'dbo'.

Dlaczego? Stanie się tak ponieważ wbrew pozorom powyższy skrypt nie nada uprawnień do wykonywania procedury pr_Fun użytkownikom z rolą public. Dzieje się tak z powodu braku komendy GO w odpowiednim miejscu.

Jeśli uruchomimy powyższy skrypt, a potem podejrzymy kod procedury (np.: sp_helptext pr_Fun) to okaże się, że polecenie GRANT EXECUTE zostało dołączone do kodu procedury, zamiast zostać wykonane. Prawidłowa wersja powyższego skryptu powinna wyglądać tak:

...
END
GO

GRANT EXECUTE ON dbo.pr_Fun TO public
GO

Niestety ale uruchamiając skrypt z takim błędem nie zostaniemy o tym poinformowani, ani ostrzeżeni. Jest to dla mnie o tyle dziwne, że słowo kluczowe END jawnie wskazuje koniec kodu procedury, a skoro GO jest wymagane to można by o tym poinformować. Po drugie próba uruchomienia skryptu z takim samym błędem, ale tworzącego funkcję nie powiedzie się z powodu takiego błędu:

Msg 156, Level 15, State 1, Procedure fn_Fun, Line 8
Incorrect syntax near the keyword 'GRANT'.

Mała rzecz ale na wszelki wypadek dobrze o tym wiedzieć.

16/12/2012

Code Retreat

Home

O Code Retreat, czyli swego rodzaju warsztatach programistycznych, usłyszałem po raz pierwszy całkiem niedawno, kiedy dyrektor działu, w którym pracuję, rzucił pomysł zorganizowania czegoś takiego w firmie. Od pomysłu do realizacji nie minęło dużo czasu i w ostatnią sobotę wraz z kolegami i koleżankami z pracy (około 20 osób) wzięliśmy udział w takich warsztatach prowadzonych przez Michała Taszyckiego.

Z czym to się je? Celem Code Retreat jest wymiana wiedzy pomiędzy uczestnikami warsztatów oraz nauka nowych rzeczy, których na co dzień nie używa się w pracy. Warsztaty takie składają się z kilku krótkich sesji, w moim przypadku było to 6 sesji po 45 minut, podczas których rozwiązuje się jakieś problemy programistyczne. Pracowaliśmy w parach i co jakiś czas wymienialiśmy się klawiaturą. W czasie każdej z sesji pary były różne, a ponieważ w wydarzeniu brali udział programiści C# i Java, czasami trzeba było programować w języku, którego nie używa się na co dzień.

Tematem przewodnim tego Code Retreat była gra w życie. W czasie każdej z sesji mieliśmy za zadanie zaprogramować taką grę, ale za każdym razem wymagania na metodykę pracy i użyte techniki programistyczne były inne. Co istotne, każda sesja kończy się usunięciem całego kody. Pogrubienie oznacza fizyczne, trwałe usunięcie kodu, a więc za każdym razem pracę rozpoczynaliśmy się od początku.

Należy podkreślić, że celem nie jest tak naprawdę napisanie działającej gry. Temat nie jest trudny, ale 45 minut to nie jest dużo czasu zważywszy na to, że przy okazji uczymy się nowych rzeczy. Główny cel to nauka, nauka i jeszcze raz nauka, w sprzyjających warunkach i bez stresu, a gra w życie czy inne zadanie to tylko pretekst.

Czego się nauczyłem? W czasie jednej z sesji mieliśmy na przykład zastosować czysto purystyczne podeście to Test Driven Development, czyli nie mogliśmy napisać linijki kodu bez testów. W innej sesji programując nie mogliśmy w ogóle używać myszki, no ewentualnie po to aby raz sprawdzić skrót i potem już go używać. W jeszcze innej sesji pisany kod powinien zawierać minimalną liczbę klauzul sterujących if, a najlepiej w ogóle itd. Zadania mogą być najróżniejsze.

Czy purystyczne trzymanie się TDD jest dobre? Uważam, że nie. Czy używanie tylko i wyłącznie klawiatury nie jest przesadę? Uważam, że dobrze znać skróty klawiaturowe i starać się minimalizować używanie myszki ale ona się też się przydaje. Czy programowanie bez if'ów ma sens, czy tak w ogóle się da? Ano da się, nie jest to może proste, ani oczywiste ale da się.

Zadania z takimi skrajnymi wymaganiami wyrywają jednak z rutyny, pokazują, że coś można zrobić inaczej, pobudzają do myślenia, rozszerzają horyzonty i to jest moim zdaniem SUPER. Niewątpliwą zaletą jest również integracja zespołu. Co by nie mówić, Code Retreat to po prostu świetna zabawa.

Podsumowując jestem bardzo zadowolony z udziału w Code Retreat. Przyznam, że początkowo byłem trochę sceptyczny, nie wiedziałem czego tak naprawdę się spodziewać, ale nie żałuję ani minuty spędzonej w sobotę w biurze :), w czym niewątpliwą zasługę ma również prowadzący Michał Taszycki.

Polecam Code Retreat!!!

19/11/2012

StackOverflowException - jak sobie z nim radzić

Home

Każdy programista, czy to C#, C++ czy Java, musiał się kiedyś spotkać z przepełnieniem stosu, który, w przypadku platformy .NET, objawia się wyjątkiem StackOverflowException. Najłatwiej do takiego błędu doprowadzić pisząc nieskończoną rekursję. W tym poście zajmę się jednak tym jak poradzić sobie z przepełnieniem stosu, kiedy napisana metoda rekurencyjna wcale nie jest nieskończona. Zacznijmy od przykładu. Załóżmy, że mamy klasę Node i przy jej pomocy tworzymy drzewo binarne, w którym dla uproszczenia będziemy przechowywać liczby. Drzewa, które rozpatrujemy w tym przypadku nie są drzewami przeszukiwań i mogą być niezrównoważone.

public class Node
{
 public uint Data { get; set; }

 public Node Left { get; set; }
 public Node Right { get; set; }
}

Napisaliśmy również metodę rekurencyjną przeszukującą zadane drzewo w poszukiwaniu węzła przechowującego określoną wartość.

public static Node FindRec(Node root, uint i)
{
 if (root == null)
  return null;

 if (root.Data == i)
  return root;

 var res = FindRec(root.Right, i);

 if (res != null)
  return res;

 return FindRec(root.Left, i);
}

Metoda ta będzie działać, ale do czasu. Utwórzmy zdegenerowane drzewo czyli takie, które w rzeczywistości jest listą. Do wygenerowania takiego drzewa użyjemy następującego kodu:

public static Node CreateDegeneratedTree(uint noOfNodes)
{
 Node root = new Node() { Data = 0 };
 Node temp = root;

 for (uint i = 0; i <= noOfNodes; ++i)
 {
  temp.Right = new Node() { Data = i };
  temp = temp.Right;
 }

 return root;
}

W przypadku wystarczająco dużej liczby węzłów w zdegenerowanym drzewie (patrz parametr noOfNodes) wywołanie FindRec zakończy się wyjątkiem StackOverflowException. Wbrew pozorom, nie musi to być jakaś ogromna wartość. Na komputerze, na którym testowałem ten kod, wystarczyło aby drzewo zawierało 10 tyś węzłów. Trochę mało, czyż nie?

W takiej sytuacji musimy zrezygnować z rekursji na rzecz poczciwej pętli. Ja bym napisał taki kod:

public static Node Find(Node root, uint i)
{
 Queue<Node> toCheck = new Queue<Node>();
 toCheck.Enqueue(root);

 while (toCheck.Count > 0)
 {
  var node = toCheck.Dequeue();

  if (node != null)
  {
   if (node.Data == i)
    return node;
    
   toCheck.Enqueue(node.Right);
   toCheck.Enqueue(node.Left);
  }
 }

    return null;
}

Korzystam w nim z pomocniczej kolekcji, do której wrzucam kolejne węzły do sprawdzenia. Pętla kręci się dopóki nie zostanie znaleziony szukany węzeł albo dopóki wszystkie węzły nie zostaną sprawdzone. Podobne podejście możemy zastosować dla innych dla innych zagadnień tego typu.

Problem z StackOverflowException można również rozwiązać w sposób nie algorytmiczny, poprzez zwiększenie domyślnego rozmiaru stosu (1MB) przy pomocy narzędzia editbin albo tworząc nowy wątek o odpowiedniej wielkości stosu. Ja był jednak tego nie robił. Jeśli można osiągnąć jakiś efekt pisząc kod w odpowiedni sposób, to należy tak zrobić. W swojej karierze nie spotkałem jeszcze przypadku kiedy musiałbym zwiększyć wielkość stosu.

Na koniec załączam jeszcze kod metody Main. Cały program pozwala pobawić się pokazanymi powyżej metodami do wyszukiwania (iteracyjną i rekurencyjną) dla zdegenerowanego drzewa o zadanej liczbie węzłów. Program umożliwia również ustalenie wielkości stosu (w celach edukacyjnych :)). Kod może i jest trochę pokraczny, ale dla celów edukacyjnych się nada. Uwaga! Podanie zbyt dużej wartości jako Stack size in kilobytes (0 for default): albo Number of nodes in tree: może zakończyć się OutOfMemoryException.

static void Main(string[] args)
{
    Console.Write("Do you want to use recursion (Y for yes)?: ");
    
    string line = Console.ReadLine();
    bool useRecursion = String.Equals(line, "Y", StringComparison.OrdinalIgnoreCase);

    if(useRecursion)
        Console.WriteLine("Recursion search will be used");

    Console.Write("Stack size in kilobytes (0 for default): ");

    line = Console.ReadLine();
    uint stackSize;
    if (!UInt32.TryParse(line, out stackSize))
        stackSize = 1024

    Console.WriteLine("Stack size is {0} kilobytes ", stackSize);

    var th = new Thread(
  new ThreadStart(() =>
  {
   Console.Write("Number of nodes in tree: ");

   while ((line = Console.ReadLine()) != String.Empty)
   {
    uint noOfNodes;
    if (UInt32.TryParse(line, out noOfNodes))
    {
     Node root = CreateDegeneratedTree(noOfNodes);

     Stopwatch sw = new Stopwatch();

     if (useRecursion)
     {
      sw.Start();
      var res = FindRec(root, noOfNodes);
      sw.Stop();
     }
     else
     {
      sw.Start();
      var res = Find(root, noOfNodes);
      sw.Stop();
     }

     root = null;
     Console.Write("Processing time (ms): {0}\n\n", sw.ElapsedMilliseconds);
    }

    Console.Write("Number of nodes in tree: ");
   }
  }), (int)stackSize * 1024);

 th.Start();
}

29/10/2012

Trochę więcej o serializacji

Home

W poprzednim poście pisałem o serializacji XML'owej, a w tym wrócę jeszcze do tematu. Skorzystamy z tego samego kodu co poprzednio ale tym razem uruchomimy go korzystając z innych serializatorów. Zamieńmy, więc XmlSerializer na SoapFormatter lub BinaryFormatter i uruchommy przykładowy kod jeszcze raz (można użyć też DataContractSerializer ale z drobną modyfikacją kodu).

Efekt końcowy będzie inny niż dla serializatora XML'owego - po deserializacji właściwość Name będzie miała taką samą wartość jak przed serializacją. To nie jedyna różnica. Jeśli do konstruktora domyślnego dodamy linijkę:

Console.WriteLine("I'm in a constructor");

i popatrzymy na liczbę wypisanych na ekran napisów I'm in a constructor to okaże się, że w przypadku serializatora XML'owego konstruktor domyślny został wywołany dwa razy (przy tworzeniu obiektu oraz przy deserializacji), a w przypadku pozostałych klas tylko jeden raz (przy tworzeniu obiektu).

Należy postawić pytanie, jak w takim razie pozostałe serializatory tworzą obiekty przy deserializacji. Przecież muszą wołać jakiś konstruktor. Otóż nie koniecznie. O ile wiem taki BinaryFormatter, pozostałe serializatory korzystają zapewne z podobnych mechanizmów, używa metody FormatterServices.GetSafeUninitializedObject, która jest odpowiedzialna za zaalokowanie odpowiednio dużego fragmentu pamięci dla obiektu zadanego typu.

Po co to wszystko? Czy nie łatwiej wywołać konstruktor? Początkowo pomyślałem, że to kwestia wydajności. Ale szybki test pokazuje, że użycie FormatterServices.GetSafeUninitializedObject jest troszkę wolniejsze niż utworzenie obiektu za pomocą konstruktora.

var s = new Stopwatch();
s.Start();

for(int i = 0; i < 1000000; ++i)
 FormatterServices.GetSafeUninitializedObject(typeof(A));

s.Stop();

Console.WriteLine(s.ElapsedMilliseconds); //Na mojej maszynie ~130 ms

s.Reset();
s.Start();

for (int i = 0; i < 1000000; ++i)
    new A();

s.Stop();

Console.WriteLine(s.ElapsedMilliseconds); //Na mojej maszynie ~20 ms
Po chwili zastanowienia doszedłem do innego wniosku. Otóż, jeśli na proces deserializacji popatrzymy jak na proces "przywracania do życia" już raz utworzonych obiektów, to ponowne wywołanie konstruktora po prostu nie ma sensu albo wręcz jest niewskazane. Macie jakieś inne pomysły? Jak sądzicie czemu jedne serializatory wołają konstruktor, a inne nie?

27/10/2012

XmlSerializer- taka ciekawostka

Home

Serializator XML'owy platformy .NET jest bardzo łatwy i przyjemny w użyciu, ale czasami jego działanie może sprowadzić nas na manowce. Poniższy kod obrazuje o co mi chodzi. Zacznijmy od przykładowej, bardzo prostej klasy, którą będziemy serializować:

public class A
{
 public string Name { get; set; }
 
 public A()
 {
  Name = "Hello"; 
 }
}

Oraz kawałka kodu:

var obj = new A() { Name = null };

var stream = new MemoryStream();
var serializer = new XmlSerializer(typeof(A));

serializer.Serialize(stream, obj);

stream.Position = 0;

var obj2 = (A)serializer.Deserialize(stream);

Kod jest bardzo prosty. Tworzymy obiekt klasy A i ustawiamy właściwość Name na null. Następnie zapisujemy obiekt do strumienia. Po zapisaniu przesuwamy wskaźnik odczytu/zapisu strumienia na początek aby móc zdeserializować obiekt. W czym tkwi haczyk? Spróbujcie odpowiedzieć na to pytanie bez zaglądania do dalszej części posta:

Jaka będzie wartość właściwości Name po odczytaniu obiektu ze strumienia?

Otóż po zdeserializowaniu właściwość Name nie będzie pusta. Serializator XML'owy odczytując obiekt ze strumienia najpierw wywoła konstruktor domyślny. Potem zobaczy, że w strumieniu właściwość Name jest pusta i stwierdzi, że w takim wypadku nie trzeba niczego przypisywać do właściwości Name deserializowanego obiektu. W konstruktorze znajduje się natomiast kod, który ustawia tą właściwości. A więc po zdeserializowaniu właściwość Name będzie zawierała ciąg znaków "Hello", czyli inną niż przed serializacją.

Teraz wyobraźmy sobie trochę bardzie skomplikowaną sytuację. Załóżmy, że mamy kod pobierający jakieś dane z bazy danych i ładujący je do odpowiednich klas. Klasy te są napisane w podobny sposób jak klasa A powyżej, czyli w konstruktorze domyślnym niektóre właściwości są inicjowane wartościami innymi niż null. Dane (obiekty) są następnie udostępniane aplikacji klienckiej przez web service'y (stare dobre ASMX, usługi WCF jeśli użyto XmlSerializerFormatAttribute)  czyli muszą być najpierw zserializowane, a potem zdeserializowane. Kod działa poprawnie.

Teraz zabieramy się za pisanie testów integracyjnych i używamy tego samego kodu odpowiedzialnego za wczytywanie danych. W momencie, kiedy chcemy użyć tych samych obiektów (tak samo stworzonych i wypełnionych danymi) co aplikacja kliencka, nawet w ten sam sposób, okazuje się, że dostajemy wyjątek NullReferenceException.

Dlaczego? Ano dlatego, że w testach integracyjnych obiekty nie zostały przesłane przez sieć, czyli nie były serializowane i deserializowane, czyli konstruktor domyślny nie został wywołany drugi raz przy deserializacji i puste właściwości nie zostały ustawione tak jak opisano powyżej.

Jak w wielu innych podobnych sytuacjach, to żadna wiedza tajemna, ale jak się o tym nie wie, może napsuć trochę krwi. Pomijam tutaj fakt, że korzystanie z opisanej "funkcjonalności", świadomie czy nie, nie jest najlepszym pomysłem.

14/10/2012

Make Object ID

Home

Z komendy Make Object ID korzystam już od bardzo dawna, nie codziennie ale w niektórych sytuacjach jest ona nieodzowna. Ostatnio zorientowałem się jednak, że nawet doświadczeni użytkownicy VS mogą o niej wiedzieć i stąd pojawił się pomysł na ten post.





Make Object ID to komenda (pokazana na powyższych zrzutach ekranu), która dostępna jest z poziomu menu kontekstowego w okienku Locals i Watch czy też z poziomu edytora kodu. Po jej wywołaniu dla danego obiektu zostanie wygenerowany identyfikator. Nadanie identyfikatora objawia się tym, że w oknie Locals czy też Watch w kolumnie Value zostanie dodany przyrostek {ID#}.



Mając taki identyfikator możemy w dowolnym momencie debugowania programu odwołać się do danego obiektu i podejrzeć jego zawartość wpisując w okienku Watch identyfikator w formacie ID#. Co najważniejsze możemy to zrobić nawet jeśli w danym momencie nie mamy dostępu (referencji) do danego obiektu:



Wyobraźmy sobie, że debugujemy problem i dochodzimy do wniosku, że stan jakiegoś obiektu zmienił się w niepowołany sposób pomiędzy jego utworzeniem/zainicjowaniem, a miejscem użycia. Niestety prześledzenie "drogi" jaką przebył obiekt krok po kroku jest bardzo trudne. W międzyczasie wywołanych zostało setki jeśli nie tysiące metod, kod jest skomplikowany, uzyskanie referencji do danego obiektu jest w wielu miejscach trudne lub niemożliwe itd. W tym przypadku Make Object ID ułatwi nam znalezienie miejsca gdzie obiekt został popsuty. W praktyce mogłoby wyglądać to tak:
  • Generujemy identyfikator dla obiektu w momencie jego inicjalizacji.
  • Stawiamy pułapkę w miejscu gdzie podejrzewamy, że obiekt jest już zmieniony.
  • Po zatrzymaniu się na pułapce sprawdzamy stan obiektu.
  • Jeśli obiekt jest zmieniony to przesuwamy pułapkę w kierunku miejsca gdzie został zainicjowany.
  • Jeśli obiekt jest niezmieniony to przesuwamy pułapkę w kierunku przeciwnym do miejsca gdzie został zainicjowany.
  • Powtarzamy procedurę.
Opisana procedura to coś w rodzaju przeszukiwania połówkowego, prosty ale skuteczny sposób. Make Object ID przyda się za każdym razem kiedy chcemy podejrzeć stan obiektu, do którego w danym momencie nie możemy się w łatwy sposób odwołać.

Na koniec dwie uwagi. Spotkałem się z informacją, że Make Object ID jest dostępne tylko w Visual Studio w wersji Ultimate. Nie mam teraz dostępu do innej wersji środowiska więc nie mogę tego zweryfikować. Po drugie, o ile wiem, nie istnieje sposób aby wylistować listę obiektów i nadanych im identyfikatorów. W praktyce trzeba więc albo zapamiętać albo zapisać sobie na kartce nadane identyfikatory.

14/09/2012

Dziwne zachowanie konstruktora statycznego - ciąg dalszy 2

Home

Niedawno kolega opowiedział mi o jeszcze jednym przypadku kiedy opisane przeze mnie zachowanie konstruktora statycznego w środowiskach x86/x64 doprowadziło do kłopotów. Scenariusz był dość ciekawy, dlatego go opiszę na uproszczonym przykładzie. Zacznijmy od tego, że napisaliśmy zarządzany komponent COM. Komponent ten w konstruktorze statycznym czyta wartość jakiegoś parametru konfiguracyjnego z pliku i na tej podstawie coś robi. W poniższym przykładzie, żeby nie komplikować sprawy, po prostu zapisuje jego wartości do pliku:

[ComVisible(true)]
public class MySimpleCOM : ServicedComponent
{
 static MySimpleCOM()
 {
  File.AppendAllText(
   @"c:\log.txt",
   String.Format("Value of configuration parameter = '{0}' in the domain '{1}'\n.", ConfigurationManager.AppSettings["Test"], AppDomain.CurrentDomain.FriendlyName));
 }
 
 public MySimpleCOM() {}
 public void Fun() {}
}

Dodam jeszcze, że do pliku AssemblyInfo.cs w projekcie z naszym zarządzanym komponentem zostały dodane takie atrybuty:

[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(false)]

ApplicationActivation zapewnia, że nasz komponent będzie hostowany poza procesem, w którym się do niego odwołamy, dokładniej przez proces dllhost.exe. Drugi atrybut jest opcjonalny ale ułatwia testy i mówi, że nie jest wymagana żadna kontrola dostępu (uprawnień) kiedy ktoś będzie próbował użyć naszego komponentu. Kod testujący nasz komponent jest jeszcze prostszy:

class Program
{
 static void Main(string[] args)
 {
  using (var host = new MySimpleCOM())
  {
   host.Fun();
  }
 }
}

Warto zwrócić uwagę, że nie trzeba samemu rejestrować komponentu. Zostanie to zrobione za nas.

Na koniec dodajemy jeszcze do pliku konfiguracyjnych programu testującego (app.config) oraz komponentu COM (Application.config) sekcję appSettings z parametrem test. Dla rozróżnienia niech wartość tego parametru różni się w obu przypadkach np.:

<appSettings>
 <add key="test" value="Tester"/>
</appSettings>

oraz

<appSettings>
 <add key="test" value="COM"/>
</appSettings>

Aby zobaczyć na czym dokładnie polega problem najłatwiej postąpić w następujący sposób. Najpierw kompilujemy oba projekty z opcją x86, uruchamiamy i zaglądamy do pliku z log.txt. Dalej kompilujemy ponownie oba projekty z opcją x64 i znowu uruchamiamy pamiętając aby uprzednio odinstalować wersję x86 komponent COM. Można to zrobić na przykład przy pomocy narzędzia Usługi składowe (dcomcnfg) wbudowanego w system Windows.

W pierwrzym przypadku (x86) plik log.txt będzie wyglądał tak:

Value of configuration parameter = 'COM' in the domain 'DefaultDomain'.

A w drugim (x64) w następujący sposób:

Value of configuration parameter = 'COM' in the domain 'DefaultDomain'.
Value of configuration parameter = 'Tester' in the domain 'Tester.vshost.exe'.

Jak widać w przypadku środowiska x64 kod inicjalizujący w konstruktorze statycznym został wywołany dwa razy, do tego z różnymi wartościami parametru konfiguracyjnego. W prawdziwym systemie mogłoby to doprowadzić do powstania jakichś błędów i chyba sami przyznacie, że ich przyczyna nie byłaby trywialna do wykrycia.

PS.

Jeśli przy własnoręcznym odtwarzaniu tego scenariusza napotkacie problem, że komponent COM nie będzie czytał konfiguracji ze pliku Application.config, to odsyłam na przykład do tego wpisu, w którym wszystko jest wyjaśnione.

22/07/2012

Jeden dziwny znak, a kilka rzeczy do zapamiętania

Home

Niedawno zetknąłem się z raportem, który wypluwał dane do pliku. Przy czym jednym z wymagań było aby trim'ować pola, które zostały dopełnione do wymaganej długości. W tym celu użyto funkcji rtrim. Przeglądając wyprodukowane raporty, zauważyłem jednak, że w niektórych przypadkach zawartość jednego pola zawiera jakieś białe znaki. Na pierszy rzut oka wyglądało to na spacje. Na wszelki wypadek sprawdziłem czy napewno zastosowano rtrim i wszystko się zgadzało.

Rozpocząłem więc poszukiwania czemu rtrim mogło nie zadziałać i od razu nauczyłem się pierwszej rzeczy, do której nie przywiązywałem wcześniej uwagi. rtrim obsługuje tylko spacje i nie poradzi sobie z tabulatorem, znakiem powrotu karetki itd. Poniżej krótki przykład demonstrujący problem (char(9) to tabulator).

DECLARE @string char(10)
SET @string = 'abc' + char(9)

SELECT LEN(@string), LEN(RTRIM(@string))

Rozwiązaniem problemu może być na przykład użycie funckji replace:

SELECT LEN(@string), LEN(RTRIM(@string)), LEN(REPLACE(@string,char(9),SPACE(0)))

W tym przypadku to jednak nie pomogło. W pliku wyjściowym cały czas pojawiały się te "dziwne" spacje. Użyłem więc prostego skryptu aby zobaczyć kody znaków dla pola, które sprawiało problemy. Okazało się, że te dziwne "spacje" to znak o kodzie 255. Teraz w zależności od tego do jakiej tablicy znaków zajrzałem uzyskałem inną informację. Na przykład według ISO 8859-1 to znak łacińska mała litera Y z diarezą/umlautem.

Tutaj dochodzimy do kolejnej ważnej rzeczy, którą łatwo przeoczyć kiedy przez większość czasu pracujemy w dobrze określonym środowisku z takimi, a nie innymi ustawieniami regionalnymi itd. Otóż ten sam znak może wyglądać inaczej kiedy wykonamy zapytanie w Microsoft SQL Server Management Studio ze względu na collation, a inaczej po zapisaniu do pliku i otwarciu w jakimś edytorze ze względu na wybraną w programie stronę kodową itd. Poniższy skrypt pokazuje jak ten sam znak zostanie zaprezentowany dla różnych collation.

DECLARE @string char(10)
SET @string = 'abc' + char(255)
SELECT @string
Collate SQL_Latin1_General_CP1_CI_AS
SELECT @string
Collate SQL_Polish_CP1250_CI_AS

Nie ma w tym nic trudnego i przez większość czasu kwestie kodowania, tablicy znaków... nas nie obchodzą ale warto pamiętać o takich rzeczach bo potem mogą pojawić się cokolwiek zaskakujący efekty, nie zawsze oczywiste do wyjaśnienia.

Post ten dotyczy MSSQL 2008.

08/07/2012

WPF i opóźnione wykonanie (deferred execution)

Home

Najpierw spójrzmy na poniższy fragment XAML'a z prostą implementacją combobox'a z wieloma kolumnami:

...
<ComboBox ItemsSource="{Binding PerformedAnalysis}" SelectedValuePath="Id" SelectedValue="{Binding SelectedItem}">
 <ComboBox.ItemTemplate>
  <DataTemplate>
   <StackPanel Orientation="Horizontal">
    <Border BorderThickness="1" BorderBrush="Black" Margin="1" Padding="1">
     <TextBlock Text="{Binding Id}" Width="100"></TextBlock>
    </Border>
    <Border BorderThickness="1" BorderBrush="Black" Margin="1" Padding="1">
     <TextBlock Text="{Binding Name}" Width="500"></TextBlock>
    </Border>
   </StackPanel>
  </DataTemplate>
 </ComboBox.ItemTemplate>
</ComboBox>
...

Oraz kod z view model'u zasilający tą kontrolkę. Kod ten używa Entity Framework, a Context to nic innego jak obiekt dziedziczący po ObjectContext.

...
private IEnumerable _PerformedAnalysis = null;

public IEnumerable PerformedAnalysis
{
    get
    {
        try
        {
            if (_PerformedAnalysis == null)
            {
                _PerformedAnalysis = 
                        from a in Context.Analyses
                        orderby a.Id
                        select new { Id = a.Id, Name = a.Name };
            }
        }
        catch (Exception ex)
        {
            ServiceProvider.GetService<IWindowService>().ShowError(ex);
            _PerformedAnalysis = new[] { new { Id = -1, Name = ex.Message } };
        }

        return _PerformedAnalysis;
    }
}
...

Kod ten zawiera dość poważną usterką. Jaką? Ktoś może powiedzieć, że błędem jest użycie bezpośrednio EF'a, zamiast ukryć dostęp do danych za warstwą interfejsów. W prostej aplikacji nie ma to moim zdaniem sensu, w bardziej rozbudowanej też można nad tym dyskutować. Ktoś może również powiedzieć, że przy bindowaniu należy użyć ObservableCollection. W tym jednak przypadku do kolekcji nie będą dodawane lub usuwane żadne elementy, więc nie jest to konieczne.

Błąd jest dużo poważniejszy i może spowodować, potocznie mówiąc, wywalenie się całej aplikacji. Odpowiedzmy na pytanie co się stanie jeśli nawiązanie połączenia z bazą danych jest niemożliwie? Oczywiście zostanie zgłoszony błąd ale czy zostanie obsłużony? Pozornie powyższy kod poradzi sobie z takim scenariusz, przecież zawiera blok try/catch.

Zapomniano jednak o tzw. opóźnionym wykonaniu (ang. deffered execution) czyli o tym, że właściwe zapytanie do bazy danych zostanie wykonane dopiero kiedy dane będą rzeczywiście potrzebne czyli kiedy silnik WPF wykona bindowanie. Inaczej mówiąc wyjątek spowodowany brakiem połączenia z bazą danych zostanie rzucony gdzieś wewnątrz silnika WPF. Będzie można go przechwycić korzystając z DispatcherUnhandledException ale z perspektywy działania aplikacji to już nic nie da.

Jak to często bywa naprawienie błędu jest bardzo proste. Należy zapewnić wykonanie zapytania jeszcze w naszym kodzie np.:

...
 _PerformedAnalysis = 
  (from a in Context.Analyses
  orderby a.Id
  select new { Id = a.Id, FileName = a.FileName, Start = a.StartTime }).ToList();
...

24/06/2012

Informatyka w służbie medycyny ;)

Home

Nie wiem jak większość z Was, ale ja na co dzień, jako programista agenta transferowego dla dużych instytucji finansowych, nie mam okazji obserwować jak wynik moich wysiłków przekłada się na pracę klienta. Sądzę, że nie jestem odosobniony i w mniejszym lub większym stopniu doświadcza tego każdy kto pracuje nad projektami przeznaczonymi dla dużych firm i korporacji. Z tego powodu lubię wykorzystywać swoje umiejętności i pomagać przyjaciołom i rodzinie.

Ostatnio moja szwagierka, która jest na specjalizacji z anestezjologii, spytała czy pomógłbym jej zautomatyzować papierkową robotę związaną ze specjalizacją. Papierologia ta przejawia się tym, że dla każdego zabiegu w jakim uczestniczy musi wypełnić odpowiedni formularz. Zabiegów takich w ciągu całego stażu musi wykonać, o ile mnie pamięć nie myli, około 2 tyś. Oznacza to konieczność wypełnienia i wydrukowania takiej samej liczby formularzy i dodatkowego zestawienia podsumowującego.

Wzór formularz przygotowała sobie sama w Excelu. Początkowe kilkadziesiąt kart wypełniła ręcznie. Robota łatwa ale żmudna i zawsze łatwo o pomyłkę. Dodatkowo ciężko potem przeglądać dużą liczbę takich kart i wyszukać tą, która nas w danej chwili interesuje. Formularz wygląda w następujący sposób:



Do tego informację o każdej takiej karcie zapisuje się w zestawieniu podsumowującym. Pomysł na automatyzację był następujący: "Chciałabym mieć arkusz w programie Excel z kolumnami, w które wpisywałabym informacje potrzebne do wypełnienia kart i zestawienia podsumowującego. Na tych kolumnach założę sobie filtry itp. Z arkusza tego generowane będę karty oraz zestawienie podsumowujące."

Zrobienie czegoś takiego nie jest trudne ale wymaga umiejętności pisania makr. Z chęcią przystąpiłem więc do pracy. Jedno popołudnie i nowe rozwiązanie było gotowe. Składa się na nie:
  • Arkusz z wzorem formularza. Z tego arkusza wzór jest kopiowany i wypełniany.
  • Arkusz z danymi. Każdy wiersz odpowiada jednemu formularzowi. Aby za każdym razem nie generować wszystkich kart zawiera dodatkową kolumnę ze znacznikiem czy już generowano daną kartę.
  • Arkusz z podsumowaniem.
  • Arkusz z wygenerowanymi kartami.
  • Skrypt (140 linii) generujący podsumowanie i karty, uruchamiany kombinacją klawiszy Ctrl+G.
Coś bardzo prostego, chwila wysiłku, ale sporo satysfakcji.

05/06/2012

CLR Profiling API - dobrego złe początki

Home

Jakiś czas temu odpowiadając na komentarz pod postem przebąknąłem, że przymierzam się do wzięcia byka za rogi i chcę napisać własny profiler korzystając z CLR Profiling API. Zagadnienie nie jest trywialne dlatego postanowiłem zacząć od zapoznania się z "literaturą". Na pierwszy ogień poszedł artykuł Write Profilers With Ease Using High-Level Wrapper Classes. Swoje lata ma, chociaż na liście mam jeszcze starsze artykuly, ale akurat w przypadku CLR  Profiling API lata biegną wolniej niż w przypadku innych technologii.

Do artykułu, jakby inaczej, dołączony jest kod, który postanowiłem jak najszybciej uruchomić aby zobaczyć czy to rzeczywiście działa. W tym momencie natknąłem się na pierwszy problem i o tym będzie ten post. Problem wynikał z tego, że chciałem "sprofilować" aplikację używającą .NET 4.0 kodem napisanym z myślą o .NET 2.0. Z biegu nie jest to możliwe i objawia się takim błędem w logu systemowym:

.NET Runtime version 4.0.30319.225 - Loading profiler failed. The profiler that was configured to load was designed for an older version of the CLR. You can use the COMPLUS_ProfAPI_ProfilerCompatibilitySetting environment variable to allow older profilers to be loaded by the current version of the CLR. Please consult the documentation for information on how to use this environment variable, and the risks associated with it. Profiler CLSID: '{B98BC3F3-D630-4001-B214-8CEF909E7BB2}'. Process ID (decimal): 5128. Message ID: [0x2517].


Łatwo jednak sobie z tym poradzić, wystarczy ustawić zmienną środowiskową COMPLUS_ProfAPI_ProfilerCompatibilitySetting. Uruchomienie programu pod kontrolą profiler'a będzie więc wyglądało w następujący sposób:

rem Rejestrujemy dll'kę z profiler'em
regsvr32 /s MyProfiler.dll

rem Włączenie profilowania
set COR_ENABLE_PROFILING=1

rem Ustawiamy profiler, który chcemy użyć (zarejestrowaliśmy go 2 linijki wcześniej)
set COR_PROFILER={32E2F4DA-1BEA-47ea-88F9-C5DAF691C94A}

rem Włączamy kombatybilność wstecz
COMPLUS_ProfAPI_ProfilerCompatibilitySetting=EnableV2Profiler

rem Uruchamiamy program
MyProgram.exe

rem Profiler nie jest już potrzebny, więc go wyrejestrowujemy
regsvr32 /s /u MyProfiler.dll

Ale to nie wszystko. Drugi rodzaj błędu z jakim się spotkałem objawia się następującym wpisem w logu systemowym:

.NET Runtime version 4.0.30319.225 - Loading profiler failed during CoCreateInstance. Profiler CLSID: '{B98BC3F3-D630-4001-B214-8CEF909E7BB2}'. HRESULT: 0x80070002. Process ID (decimal): 2620. Message ID: [0x2504].

I może wynikać z dwóch rzeczy. Po pierwsze jeśli nasz program jest programem 64 bitowym, do jego profilowania musimy użyć 64 bitowej wersji profilera. Sprawa ma się podobnie, jeśli program jest 32 bitowy. W przeciwnym wypadku środowisko nię będzie potrafiło załadować odpowiedniej biblioteki. Błąd pojawi się również jeśli spróbujemy uruchomić profiler nie posiadając praw administratora (Windows Vista z włączonym UAC). A więc uruchamiając skrypt należy skorzystać z opcji Uruchom jako administrator.

24/05/2012

Krótka lekcja na temat char i varchar

Home

Na początek prosty kawałek kodu w T-SQL, w którym sprawdzane jest, czy zadany ciąg znaków pasuje do podanego wzorca tj. czy zaczyna się dwoma cyframi:

declare @input char(10)
declare @pattern char(100)

SET @input = '12aaabbb'
SET @pattern = '[0-9][0-9]%'

if @input like @pattern
 print 'OK'
else
 print 'Fail'

Pomimo, że ciąg znaków pasuje do wzorca to warunek dopasowania nie jest spełniony i na ekran zostanie wypisany napis Fail. Dzieje się tak ponieważ kiedy przypisujemy wzorzec do zmiennej @pattern, która jest typu char(100) to zostanie on dopełniony spacjami. A więc przy testowaniu warunku tak naprawdę sprawdzamy czy zadany ciąg znaków zaczyna się dwoma cyframi i kończy przynajmniej 89 spacjami (100 - długość wzorca).

Można to naprawić przechowując wzorzec w zmiennej typu varchar albo stosując funkcję rtrim. Wszystko zależy od konkretnej sytuacji ale przy wykonywaniu różnych operacji na ciągach znaków należy zawsze pamiętać o różnicy pomiędzy char i varchar. Sprawa wydaje się prosta ale kiedy pracujemy z dużą ilością kodu bazodanowego łatwo można coś przeoczyć, a znalezienie takiego błędu może nie być wbrew pozorom proste.

13/05/2012

Podglądanie tabel tymczasowych

Home

Czasami zdarza się, że pracujemy z aplikacją, która tworzy tabelę tymczasową, a następnie woła serię procedur składowanych, które: wypełniają tę tabelę, modyfikują ją itd. W takim wypadku dość często np.: przy debugowaniu błędu, potrzeba podejrzeć zawartość takiej tabeli. Pół biedy kiedy jest to globalna tabela tymczasowa np.: ##SomeTempTable. Wtedy wystarczy zatrzymać aplikację na pułapce i wykonać zapytanie w SQL Server Management Studio np.: select * from ##SomeTempTable.

Niestety, jeśli globalna tabela tymczasowa została utworzona w transakcji, to już nie zadziała. Jeszcze gorzej jest w przypadku lokalnych tabel tymczasowych np.: #SomeTempTable, których nie podejrzymy nawet jeśli zostały utworzone poza transakcją. Co w takim wypadku?

Ja w takiej sytuacji korzystam z możliwości Visual Studio, a dokładniej z potęgi okna Immediate (Debug -> Windows -> Immediate Ctrl + D, I). Zakładam, że wykonując poniższe kroki aplikacja zatrzymana jest na jakiejś pułapce np.: przed uruchomieniem kolejnej procedury robiącej coś z tabelą tymczasową.
  • W oknie Immediate wpisuje DataTable dt = new DataTable();
  • Również w oknie Immediate, wpisuję dt = DataProvider.Instance.ExecuteDataTable("select * from #SomeTempTable");.
  • Przechodzę np.: do okna Watch, podaję nazwę utworzonej zmiennej czyli dt i cieszę się wbudowanym w VS wizualizatorem dla klasy DataTable.


Kilka słów wyjaśnienia w sprawie DataProvider.Instance.ExecuteDataTable. Zakładam tutaj, że jeśli ktoś pisze aplikację bazodanową i nie korzysta z ORM, to ma napisaną jakąś pomocnicza klaskę, która: zarządza połączeniem do bazy danych, pozwala łatwo wykonać zapytania bez potrzeby każdorazowego ręcznego tworzenia DbCommand itd. W tym przypadku jest to klasa DataProvider, która jest singleton'em. Jest to o tyle ważne, że kiedy odwołuję się do niej w oknie Immediate to korzystam z tego samego połączenia co aplikacja, a więc mogę podejrzeć tabele tymczasowe. Pod spodem wykonywane jest zwykłe zapytanie w stylu ADO.NET.

Ostatnio odkryłem również fajny projekt sp_select składający się z dwóch procedur składowanych, które w "magiczny" sposób pozwalają podejrzeć lokalną tabelę tymczasową korzystając z innego połączenia niż to, w którym tabela została utworzona.

09/05/2012

Dziwne zachowanie konstruktora statycznego - ciąg dalszy

Home

Na początku marca pisałem o "dziwnym" zachowaniu konstruktora statycznego. W skrócie chodziło o to, że jeśli chcieliśmy wywołać metodę w domenie aplikacyjnej innej niż domyślna, konstruktor statyczny klasy, do której należała ta metoda, wołany był o jeden raz więcej w środowisku x64 niż w środowisku x86. Ponieważ sprawa mnie nurtowała postanowiłem zadać pytanie poprzez portal Microsoft Connect. Zajęło to trochę ale w końcu uzyskałem wyjaśnienie. Odpowiedź jest dość ciekawa, a więc zapraszam do lektury.

08/05/2012

RavenDB (cz. 7) - HttpListenerException

Home

Ten post będzie krótki i zwięzły. Jakiś czas temu kiedy włączyłem UAC (User Account Control) ze zdziwieniem zauważyłem, że moja aplikacja używająca Raven DB nie działa. Przy wywołaniu metody DocumentStore.Initialize rzucany był wyjątek HttpListenerException. Po wyłączeniu UAC błąd nie występował.

Z problem łatwo sobie poradzić nadając użytkownikowi, jaki uruchamia aplikację, uprawnienia do nasłuchiwania na porcie używanym przez bazę danych. Można to zrobić przy pomocy narzędzia httpcfg lub netsh tak jak to zostało opisane w tym dokumencie. Co jednak najlepsze Raven DB dostarcza gotowej klasy NonAdminHttp, w bibliotece Raven.Database.dll, która rozwiązuje ten problem. Poniżej przykład użycia.

...
NonAdminHttp.EnsureCanListenToWhenInNonAdminContext(Store.Configuration.Port);

Store.Initialize();
...

A tak w ogóle to warto zajrzeć, przy pomocy jakiegoś deasemblera, do wnętrza metody EnsureCanListenToWhenInNonAdminContext i zobaczyć jak została zaimplementowana.

Podsumujmy co już umiemy:
  • Osadzić Raven DB w aplikacji hostującej.
  • Zainicjować Raven DB.
  • Skonfigurować dostęp do Raven Studio i API REST'owego.
  • Tworzyć obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Dodawać/usuwać/modyfikować dokumenty.
  • Zadawać proste i te trochę bardziej skomplikowane zapytania.
  • Utworzyć indeks.
  • Skorzystać z algorytmu Map/Reduce.
  • Skorzystać z zapytań Lucene.
  • Wymusić zwrócenie przez zapytanie aktualnych danych.
  • Sterować tym, które właściwości zostaną zapisane do bazy danych.
  • Rozwiązać kłopoty związane z IntelliTrace i Raven DB.
  • Rozwiązać problem z HttpListenerException.

12/04/2012

Programmable Data Query (cz.2)

Home

W poprzednim poście opisałem czym są Programmable Data Query i jak zdefiniować wykorzystujące je zdarzenie IntelliTrace. Teraz pora na przedstawienie przykładowej implementacji PDQ. Zacznijmy jednak od przypomnienia do czego służą poszczególne metody interfejsu IProgrammbleDataQuery. Potrzebne informacje zamieściłem na poniższej mapie myśli:



Poniższe PDQ może zostać użyte z każdym zdarzeniem diagnostycznym. Jest proste ale nie znajdziemy analogicznego w bibliotece Microsoft.VisualStudio.DefaultDataQueries.dll. Bardzo dobrze nadaje się jako baza to tworzenia bardziej skomplikowanych rozwiązań.

Jego działanie polega na odczytaniu wartości parametrów aktualnych wywołania metody lub wartości zwróconej przez metodę i stworzeniu na tej postawie opisu zdarzenia. Dla przypomnienia opis zdarzenia wyświetlany jest przez Visual Studio w czasie przeglądania nagranego logu IntelliTrace.

public class Test : IProgrammableDataQuery
    {
        public object[] EntryQuery(object thisArg, object[] args)
        {
            return args;
        }

        public object[] ExitQuery(object returnValue)
        {
            return new object[] { returnValue };
        }

        public List<CollectedValueTuple> FormatCollectedValues(object[] results)
        {
            List<CollectedValueTuple> list = new List<CollectedValueTuple>();
            for (int i = 0; i < results.Length; ++i)
            {
                list.Add(new CollectedValueTuple(
                    String.Format("Result{0}", i), 
                    String.Format("'{0}'",results[i]), 
                    "string"));
            }
            return list;
        }

        public string FormatLongDescription(object[] results)
        {
            try
            {
                StringBuilder builder = new StringBuilder();
                builder.AppendFormat("Collected objects ({0}): ", results.Length);
                bool flag = true;
                foreach (var r in results)
                {
                    if (!flag)
                    {
                        builder.Append(", ");
                    }

                    flag = false;
                    builder.AppendFormat("'{0}'", r);
                }

                return builder.ToString();
            }
            catch (Exception ex)
            {
                return ex.ToString();
            }
        }

        public string FormatShortDescription(object[] results)
        {
            return FormatLongDescription(results);
        }

        public List<Location> GetAlternateLocations(object[] results)
        {
            return null;
        }
    }
Implementacja EntryQuery oraz ExitQuery jest trywialna. W żaden sposób nie modyfikuję danych wejściowych i po prostu przekazuje je dalej. FormatLongDescription wykorzystuje te dane do stworzenia opisu zdarzenia po prostu sklejając poszczególne elementy tablicy. Dla zdarzenia przeznaczonego do analizowania danych wejściowych będą to parametry aktualne, a dla zdarzenia przeznaczonego do analizowania danych wyjściowy wynik zwrócony przez metodę. FormatShortDescription po prostu wywołuje FormatLongDescription. Logika FormatCollectedValues też nie jest skomplikowana. Metoda ta po prostu zwraca dane jakie otrzymała na wejściu, dodatkowo nadając im etykiety Result0, Result1 itd.

07/04/2012

Programmable Data Query

Home

O zdarzeniach IntelliTrace pisałem już kilkakrotnie (Własne zdarzenia IntelliTrace!, Własne zdarzenia IntelliTrace 2). Każdy z tych postów dotyczył jednak zdarzeń definiowanych deklaratywnie w pliku XML. Jest to stosunkowo proste, nie potrzeba nic kodować ale co z tym związane ma to też swoje ograniczenia.

W takiej sytuacji z pomocą przychodzą nam Programmable Data Query (w skrócie PDQ) czyli klasy implementujące interfejs Microsoft.HistoricalDebuggerHost.IProgrammableDataQuery. Interfejs ten umożliwia programowe analizowanie zdarzeń IntelliTrace (wywołań metod), parametrów aktualnych wywołań, właściwości obiektów itd. Daje to bardzo duże pole do popisu, zacznijmy jednak od tego jak zdefiniować zdarzenie korzystające z PDQ w pliku z planem działania IntelliTrace (domyślnie CollectionPlan.xml):
<DiagnosticEventSpecification xmlns="urn:schemas-microsoft-com:visualstudio:tracelog" enabled="true">
 <Bindings>
  <Binding onReturn="false">
   <ModuleSpecificationId>TestApp.exe</ModuleSpecificationId>
   <TypeName>TestApp.A</TypeName>
   <MethodName>Fun</MethodName>
   <MethodId>TestApp.A.Fun(System.Int32):System.Void</MethodId>
   <ShortDescription _locID="shortDescription.TestApp.A.Fun(System.Int32):System.Void"></ShortDescription>
   <LongDescription _locID="longDescription.TestApp.A.Fun(System.Int32):System.Void"></LongDescription>
   <DataQueries>
   </DataQueries>
   <ProgrammableDataQuery>
    <ModuleName>IntelliTrace.ProgrammableDataQueries.dll</ModuleName>
    <TypeName>IntelliTrace.ProgrammableDataQueries.Test</TypeName>
   </ProgrammableDataQuery>
  </Binding>
 </Bindings>
 <CategoryId>Test</CategoryId>
 <SettingsName _locID="settingsName.TestApp.A.Fun(System.Int32):System.Void">Fun</SettingsName>
 <SettingsDescription _locID="settingsDescription.TestApp.A.Fun(System.Int32):System.Void">Fun</SettingsDescription>
</DiagnosticEventSpecification>
Nie będę dokładnie omawiał co oznaczają poszczególne węzły XML ponieważ, ponieważ zrobiłem to we wcześniejszych postach. W skrócie, powyższe zdarzenie zostało zdefiniowane dla metody o sygnaturze void Fun(int), której należy szukać w dll'ce TestApp.exe. Jedyna nowość to użycie węzła ProgrammableDataQuery zamiast DataQueries, który nie robi nic innego jak wskazuje PDQ. Zawiera on dwa podwęzły, których znaczenia łatwo się domyśleć. ModuleName to pełna nazwa dll'ki zawierającej klasę z implementacją interfejsu IProgrammableDataQuery, a TypeName definiuje pełną nazwę tej klasy.

Wróćmy do tego co najciekawsze czyli do implementacji interfejsu IProgrammableDataQuery. Deklarację tego interfejs znajdziemy w bibliotece Microsoft.VisualStudio.IntelliTrace.dll, który u mnie na komputerze leży w poniższym katalogu:

C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies\

Po dodaniu do projektu referencji do powyższej biblioteki i zaimportowaniu przestrzeni nazw nie pozostaje nam nic innego jak zabrać się do implementacji poszczególnych metod. Nie jest ich dużo. Pierwsza grupa metod wołana jest w czasie nagrywania logu IntelliTrace, w momencie pojawienia się zdarzenia - wywołania metody:
  • object[] EntryQuery(object thisArg, object[] args) - Metoda wołana jeśli mamy do czynienia ze zdarzeniem przeznaczonym do analizowania danych wejściowych (pisałem o tym w tym poście). thisArg to obiekt na rzecz, którego została wywołana metoda, a args to wartości parametrów przekazanych do metody. Tablica zwrócona przez EntryQuery zostanie następnie przekazana do FormatShortDescription, FormatLongDescription oraz FormatCollectedValues.
  • object[] ExitQuery(object returnValue) - Metoda wołana jeśli mamy do czynienia ze zdarzeniem przeznaczonym do analizowania danych wyjściowych. returnValue to wynik zwrócony przez metodę. Tablica zwrócona przez ExitQuery zostanie następnie przekazana do FormatShortDescription, FormatLongDescription oraz FormatCollectedValues.
Drugra grupa metod wołana jest w czasie przeglądania logu IntelliTrace na przyład w Visual Studio:
  • List<CollectedValueTuple> FormatCollectedValues(object[] results) - Metoda ta pozwala sformatować dane skojarzone ze zdarzeniem, zwrócone przez EntryQuery albo ExitQuery. Dane te będą potem wyświetlane w Visual Studio po wybraniu danego zdarzenia. Metoda ta powinna więc przynajmniej zwrócić to co otrzymała na wejściu tak aby Visual Studio miało co pokazać.
  • string FormatLongDescription(object[] results) - Ta metoda zwraca tzw. długi opis zdarzenia wyświetlany przez Visual Studio. Jako dane wejściowe przyjmuje tablicę zwróconą przez EntryQuery lub ExitQuery.
  • string FormatShortDescription(object[] results) - Ta metoda zwraca tzw. krótki opis zdarzenia wyświetlany przez Visual Studio. Jako dane wejściowe przyjmuje tablicę zwróconą przez EntryQuery lub ExitQuery.
  • List<Location> GetAlternateLocations(object[] results) - Szczerze mówiąc jeszcze dokładnie nie wiem jak użyć tej metody ale jak tylko się dowiem to o tym napiszę :)
Gotową dll'kę z naszą własną implementacją PDQ musimy umieścić w katalogu, w którym znajduje się program IntellITrace.exe. Domyślna lokalizacja to:

VS_2010_INSTALL_DIR\Team Tools\TraceDebugger Tools.

Uwaga! Tak jak pisałem dll'ka z PDQ potrzebna jest nie tylko w czasie nagrywania logu ale również w czasie jego przeglądania. Jeśli będzie jej brakować informacje na temat nagranych zdarzeń nie będą dostępne.

Powyższy katalog zawiera również bardzo ciekawą bibliotekę Microsoft.VisualStudio.DefaultDataQueries.dll, w której znajdziemy kilkadziesiąt przykładowych PDQ. Analizując ten kod można się dużo dowiedzieć. Na koniec jeszcze jedna informacja. PDQ zadziałają również jeśli uruchomimy IntelliTrace poza Visual Studio (o tej technice pracy z IntelliTrace pisałem w tym poście).

W następnym poście przedstawię przykładową implementację PDQ.

29/03/2012

RavenDB (cz. 6) - małe kłopoty z IntelliTrace

Home

Ten post będzie krótki ale poruszę w nim sprawę, o której dobrze wiedzieć aby potem nie kląć pod nosem i nie wołać o pomstę do nieba, bo coś nagle przestało działać.

Otóż Raven DB, z powodów opisanych dalej, nie współpracuje dobrze z historycznym debugerem IntelliTrace pracującym w trybie rozszerzonym (IntelliTrace events and call information). Jest to tryb, w którym IntelliTrace monitoruje wywołania metod, konstruktorów, dostęp do właściwości itd. oraz dodatkowo tzw. zdarzenia diagnostyczne, które są monitorowane również w trybie podstawowym (pisałem o tym w poście).

Tak naprawdę problem nie jest związany bezpośrednio z Raven DB ale z jedną z bibliotek z jakich korzysta. Nie współpracuje to zresztą eufemizm, bo powinienem napisać nie działa, wywala się... Jeśli uruchomimy aplikację korzystającą z Raven DB pod kontrolą IntelliTrace w pewnym momencie (próba zapisu, odczytu, utworzenia indeksu) otrzymamy wyjątek VerificationException z komunikatem Operation could destabilize the runtime.. Call stack zaprowadzi nas natomiast do biblioteki Newtonsoft.Json.

IntelliTrace wstrzykuje w kod monitorowanych programów własne instrukcje i to najpewniej w tym przypadku powoduje błąd. Z problemem można sobie jednak łatwo poradzić mówiąc IntelliTrace, aby ignorował tą bibliotekę. W tym celu otwieramy okno opcji Tools -> Options, wybieramy menu IntelliTrace i dalej Modules, klikamy przycisk Add..., i w polu tekstowym wpisujemy *Newtonsoft*, a na koniec zatwierdzamy.

Podsumujmy co już umiemy:
  • Osadzić Raven DB w aplikacji hostującej.
  • Zainicjować Raven DB.
  • Skonfigurować dostęp do Raven Studio i API REST'owego.
  • Tworzyć obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Dodawać/usuwać/modyfikować dokumenty.
  • Zadawać proste i te trochę bardziej skomplikowane zapytania.
  • Utworzyć indeks.
  • Skorzystać z algorytmu Map/Reduce.
  • Skorzystać z zapytań Lucene.
  • Wymusić zwrócenie przez zapytanie aktualnych danych.
  • Sterować tym, które właściwości zostaną zapisane do bazy danych.
  • Rozwiązać kłopoty związane z IntelliTrace i Raven DB.

17/03/2012

RavenDB (cz. 5) - JsonIgnore

Home

Kiedy zapisujemy w Raven DB jakiś obiekt, to domyślnie w bazie zostaną zapisane wartości wszystkich jego właściwości, publicznych i prywatnych, a także tych tylko do odczytu. Nie zawsze jest to pożądane, niektóre rzeczy chcemy po prostu pominąć. W takiej sytuacji z pomocą przychodzi nam atrybut JsonIgnore. Właściwości oznaczone tym atrybutem będą pomijane przez silnik serializujący, a znajdziemy go w dll'ce Newtonsoft.Json.dll używanej przez Raven DB.
public class Test
{
 public int Id { get; set; }
 public int string WillBeSavedToRavenDB{ get; set; }
 [JsonIgnore]
 public int string WillBeIgnoredByRavenDB{ get; set; }
}
Użycie atrybutu JsonIgnore może być jednak problematyczne. W swoich projektach używam klasy BaseEntity, która jest klasą bazową dla innych encji np.: ExpressionEntity, TranslationEnity itd. Klasa ta zdefiniowana jest w osobnej bibliotece nie mającej niż wspólnego z Raven DB. W szczególności wykorzystuję ją w projektach, które korzystają z relacyjnej bazy danych.

Łatwo się domyślić, że klasa ta ma właściwości, których nie chcę zapisywać w dokumentowej, lub innej, bazie danych. Innymi słowy wymaga to abym oznaczył je atrybutem JsonIgnore czyli dodał do projektu zawierającego tą klasę referencję do biblioteki Newtonsoft.Json.dll. Nie chciałem jednak tego robić, bo jest to atrybut specyficzny dla Raven DB i biblioteka ta nie jest potrzebna we wszystkich moich projektach.

Problem rozwiązałem oznaczając interesujące właściwości jako virtual, umożliwiając tym samym ich przedefiniowanie (ang. override) i oznaczenie atrybutem JsonIgnore w projektach używających Raven DB.
public class BaseEntity
{
 public virtual string Name { get; set; }
}

public class TestEntity : BaseEntity
{
 public int Id { get; set; }
 public string Id { get; set; }
 [JsonIgnore]
 public override string Name
 {
  get { return base.Name; }
  set { base.Name = value; }
 }
}
Niestety z moich obserwacji wynika, że opisane podejście nie działa z właściwościami protected. Jeśli przedefiniowujemy taką właściwość i oznaczamy atrybutem JsonIgnore to zostanie to zignorowane, a jej wartość zostanie zapisana w bazie danych. Czyżby bug w Raven DB?

Podsumujmy co już umiemy:
  • Osadzić Raven DB w aplikacji hostującej.
  • Zainicjować Raven DB.
  • Skonfigurować dostęp do Raven Studio i API REST'owego.
  • Tworzyć obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Dodawać/usuwać/modyfikować dokumenty.
  • Zadawać proste i te trochę bardziej skomplikowane zapytania.
  • Utworzyć indeks.
  • Skorzystać z algorytmu Map/Reduce.
  • Skorzystać z zapytań Lucene.
  • Wymusić zwrócenie przez zapytanie aktualnych danych.
  • Sterować tym, które właściwości zostaną zapisane do bazy danych.

03/03/2012

Domeny aplikacyjne, konstruktor statyczny, a platforma x86 vs x64

Home

Na początek trochę kodu. Zacznijmy od klasy testowej:
public class TestClass : MarshalByRefObject
{
    static TestClass()
    {
        Console.WriteLine(String.Format("I'm in the static constructor in the domain '{0}'.",
            AppDomain.CurrentDomain.FriendlyName));
    }

    public void Hello()
    {
        Console.WriteLine(String.Format("Hello from the domain '{0}'.", AppDomain.CurrentDomain.FriendlyName));
    }
}
Teraz kod testujący:
AppDomain domain = AppDomain.CreateDomain("Test");

TestClass t = (TestClass)domain.CreateInstanceAndUnwrap(typeof(TestClass).Assembly.FullName, typeof(TestClass).FullName);
t.Hello();
Oraz pytanie co zostanie wypisane na ekran? A w szczególności ile razy zostanie wywołany konstruktor statyczny? W głównej domenie aplikacyjnej? W domenie pomocniczej? A może w obu?

Skoro o to pytam to zapewne gdzieś tkwi haczyk. Otóż okazuje się, że wynik będzie zależał od tego czy program został skompilowany z opcję Platform target ustawioną na x86 czy x64. W przypadku x86 na ekran zostanie wypisany taki wynik:

I'm in the static constructor in the domain 'Test'.
Hello from the domain 'Test'
A w przypadku x64 taki:
I'm in the static constructor in the domain 'Test'.
I'm in the static constructor in the domain 'ConsoleApplication.vshost.exe'.
Hello from the domain 'Test'
Jeśli natomiast program zostanie skompilowany z opcją AnyCPU wynik będzie zależał od maszyny na jakiej go uruchomimy.

Konstruktor statyczny dla danej klasy wołany jest co najwyżej jeden raz w danej domenie aplikacyjnej. Jak jednak widać w zależności od platformy może zostać wywołany w jednej lub dwóch domenach. Może to mieć znaczenie kiedy jego kod będzie zawierał np.: jakiś kod inicjalizujący. Przedstawiony scenariusz nie jest zbyt częsty ale jeśli wystąpi, wykrycie błędu może być trudne, dlatego dobrze wiedzieć o tej różnicy.

Opisane zachowanie testowałem na trzech maszynach więc zakładam, że nie jest to coś lokalnego i przypadkowego.