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.

19/02/2012

RavenDB (cz. 4) - zapytania 2

Home

Ten post to kontynuacja poprzedniego postu dotyczącego zapytań w Raven DB.

Contains == Equals !!!

Jedną z funkcjonalności jaką chciałem zaimplementować w swoim programie było wyszukiwanie wyrażeń/tłumaczeń zawierających podany ciąg znaków. Brzmi prosto ale nie obyło się bez problemów. Otóż okazało się, że Raven DB traktuje wywołanie metody String.Contains jako String.Equals, chyba że do poszukiwanego ciągu znaków dodamy gwiazdki. Na przykład zamiast s.Contains("kot") użyjemy s.Contains("kot*"), s.Contains("*kot") lub s.Contains("*kot*") w zależności czy poszukiwane wyrażenie ma znajdować się na początku, na końcu lub w środku.
string textToFind = String.Format("*{0}*", textToFind);

var res = 
 from ex in session.Query<ExpressionEntity>()
 where ex.Expression!= null && ex.Expression.Contains(textToFind) || ex.Translations.Any(t => t.Translation != null && t.Translation.Contains(textToFind))
 orderby ex.Expression
 select ex
Pozostaje jeden problem, którego niestety nie udało mi się rozwiązać. Ten kod nie zadziała jeśli szukany tekst zawiera białe znaki. Załóżmy, że na liście naszych wyrażenie mamy wyrażenie Ala ma kota. Jeśli spróbujemy wyszukać "*Ala*", "*ma*", "*kota*" otrzymamy poprawny wynik. Jeśli natomiast spróbujemy wyszukać całe wyrażenie "*Ala ma kota*" zapytanie zwróci nam pustą listę. Co dziwniejsze jeśli nie użyjemy gwiazdek czyli zlecimy wyszukanie "Ala ma kota" wyrażenie zostanie znalezione.

Ale co, jeśli szukam podwyrażenia "Ala ma" lub "ma kota"? Nie jest to duże pocieszenie ale Raven DB poradzi sobie z "Ala ma*" (z "*ma kota" już nie). Moim zdaniem sugeruje to, że wewnętrznie Raven DB do wyszukiwania używa jakiejś struktury opartej o prefixy.

Małe ostrzeżenie

W jednym z zapytań chciałem użyć metody String.Equals ale zakończyło się to wyjątkiem NullReferenceException. Sądzę, że jest to związany z użyciem metod statycznych bo kiedy użyłem metody instancyjnej np.: ex.Expression.Equals(textToFind, StringComparison.InvariantCultureIgnoreCase) obyło się bez błędów.

Stale Data

Jedną z głównych idei przyświecających twórcom Raven DB było zapewnić aby pytający o dane jak najszybciej uzyskał odpowiedź nawet jeśli oznaczałoby to zwrócenie nieaktualnych, przestarzałych danych. Używając Raven DB trzeba być na to przygotowanym nawet pracując z małą ilością danych. W moim przypadku baza danych zawiera ok. 600 dokumentów, nie całe 0.5 MB po wyeksportowaniu do pliku XML i obserwuję ten efekt.

Na przykład na początku moja aplikacja wyświetlała liczbę wyrażeń obok etykiety z nazwą języka. Jeśli jednak użytkownik importował do aplikacji wiele wyrażeń to operacja zapisywania danych, odświeżania indeksów itd. po stronie bazy danych trwała na tyle długo, że wyliczona liczba wyrażeń nie zgadzała się ze stanem faktycznym.
  • Użytkownik rozpoczyna import wyrażeń.
  • Zakończenie importu z perspektywy aplikacji/użytkownika.
  • Obliczenie liczby wyrażeń w poszczególnych językach.
  • ...
  • Faktyczne zakończenie importu po stronie bazy danych.
W tej chwili do problemu podchodzę tak, że liczbę słów w danym języku wyświetlam na życzenie. Jeśli takie zachowanie Raven DB jednak nam przeszkadza to należy explicite powiedzieć, że nie śpieszy się nam i możemy poczekać na aktualne dane np.:

var res = 
 from ex in session.Query<ExpressionEntity>().Customize(a => a.WaitForNonStaleResults())
 select ex;

Lucene

Raven DB jako silnika indeksujący używa technologii Lucene .NET. Co ciekawe umożliwia zdefiniowanie zapytań w składni jaką posługuje się ten silnik. Bezpośrednie użycie Lucene może wyglądać tak:
var res = session.Advanced.LuceneQuery<ExpressionEntity>().Where(luceneQuery);
Gdzie luceneQuery to po prostu ciąg znaków zawierający zapytanie w składni zrozumiałej dla Lucene. Może to się przydać kiedy będziemy chcieli zadać zapytanie nie obsługiwane przez "LINQ to Raven DB".

Indeksy

Jeszcze parę słów o indeksach. Termin ten przewinął sie już kilka razy. Pracując z Raven DB trzeba sobie przede wszystkim uświadomić, że tutaj indeksy różnią się od indeksów używanych w bazach relacyjnych. Moim zdaniem dwie różnice są fundamentalne. Po pierwsze Raven DB indeksuje zapytania, a nie dane. Indeks definiujemy definiując zapytanie, a nie wskazując na przykład atrybut dokumentu, który ma zostać zaindeksowany.

Po drugie Raven DB używa indeksów przy wykonywaniu każdego zapytania nawet jeśli taki indeks nie został zdefiniowany. W takiej sytuacji tworzony jest indeks dynamiczny. Druga grupa indeksów to indeksy statyczne, zdefiniowane explicite przez użytkownika na podstawie jego wiedzy o zadawanych pytaniach. Tymczasowe indeksy dynamiczne mogą zostać zamienione na stałe jeśli Raven DB zauważy, że taki indeks jest często używany.

Podsumowanie

O ile sposób realizacji podstawowych operacji dodaj/usuń/zmodyfikuj na dokumentach bardzo chwaliłem ze względu na jego prostotę i niski koszt wejścia to o raportowaniu/zapytaniach nie mogę już tego powiedzieć. Z jednej strony nie jest to żaden rocket science ale z drugiej nie jest to już "bułka z masłem" i wymaga znajomości rożnych sztuczek i specyfiki Raven DB. Jeśli uwzględnić, że zapytania jakich użyłem były bardzo proste, a pomimo to sprawiły mi sporo kłopotów to obawiam się, chociaż są to tylko moje przypuszczenia, że realizacja skomplikowanych raportów może być w Raven DB bardzo nietrywialna.

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.