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.