11/05/2010

Własne zdarzenia IntelliTrace!

Home

IntelliTrace, znane również pod nazwą historycznego debugger'a, to narzędzie jakie pojawiło się w Visual Studio 2010, a które stanowi rozwinięcie "tradycyjnych" debugger'ów o możliwość nagrywania historii wykonania programu w celu jej późniejszej analizy. Post ten rozpoczyna serię dotyczącą tej technologii, a w której chcę opisać zaawansowane techniki użycia IntelliTrace.

IntelliTrace posiada dwa tryby pracy: podstawowy oraz rozszerzony. W trybie podstawowym stan programu zapisywany jest w momencie wystąpienia tzw. zdarzeń diagnostycznych. W trybie rozszerzonym rejestrowane są dodatkowo wywołania metod, dostęp do właściwości itd. Zdarzenia diagnostyczne to ważne punkty w historii wykonania programu np.: wykonanie zapytania do bazy danych, dostęp do pliku, wystąpienie wyjątku, zatrzymanie programu na pułapce i wiele innych. Poniższy rysunek pokazuje penel z listą dostępnych zdarzeń. Możemy się do niego dostać wybierając Tools->Options->IntelliTrace->IntelliTrace Events.



Większość zdarzeń związana jest z wywołaniem określonych metod np.: ExecuteReader. Zdarzenia tego rodzaju zdefiniowane są w pliku CollectionPlan.xml. Istotne jest to, że plik ten można modyfikować poprzez zmianę istniejących zdarzeń albo dodawanie właśnych. Plik ten znajduje się w lokalizacji:

VS_2010_INSTALL_DIR\Team Tools\TraceDebugger Tools\LANGUAGE\CollectionPlan.xml

VS_2010_INSTALL_DIR to katalog instalacyjny Visual Studio 2010. Wartość LANGUAGE zależy od używanego przez nas języka. Domyślnie po zainstalowaniu środowiska zostanie utworzony katalog en. Jeśli jednak pracujemy z językiem polskim musimy samemu utworzyć katalog o nazwie pl i skopiować do niego plik CollectionPlan.xml. Jest to konieczne jeśli chcemy wprowadzić jakieś zmiany do podstawowego zestawu zdarzeń. Przed przystąpieniem do pracy zalecam utworzenie kopii zapasowej tego pliku.

Modyfikację ColllectionPlan.xml pokażę na przykładzie dodania nowego zdarzenia do kategorii Console. Kategoria ta zawiera zdarzenia skojarzone z wywołaniem metody Console.WriteLine. Z niewiadomych powodów nie przewidziano zdarzenia dla metody Console.Write. Naprawmy to! Poniżej zamieściłem XML stanowiący definicję nowego zdarzenia dla tej metody:

      <DiagnosticEventSpecification enabled="false">
        <CategoryId>console</CategoryId>
        <SettingsName _locID="settingsName.Console.Write">Write (1 args)</SettingsName>
        <SettingsDescription _locID="settingsDescription.Console.Write">Console Output with a String passed in.</SettingsDescription>
        <Bindings>
          <Binding>
            <ModuleSpecificationId>mscorlib</ModuleSpecificationId>
            <TypeName>System.Console</TypeName>
            <MethodName>Write</MethodName>
            <MethodId>System.Console.Write(System.String):System.Void</MethodId>
            <ShortDescription _locID="shortDescription.Console.Write.String">{0}</ShortDescription>
            <LongDescription _locID="longDescription.Console.Write">Console Output "{0}".</LongDescription>
            <DataQueries>
              <DataQuery index="0" maxSize="256" type="String" name="value" _locID="dataquery.Write.value" _locAttrData="name" query=""></DataQuery>
            </DataQueries>
          </Binding>
        </Bindings>
      </DiagnosticEventSpecification>

Węzeł Category określa kategorię do jakiej zostanie dodane nowe zdarzenie. SettingsName definiuje nazwę zdarzenia jaka będzie widoczna w ustawieniach VS 2010. Kolejny węzeł o nazwie SettingsDescription ma podobne znaczenie. Zawiera opis zdarzenia jak będzie wyświetlany dla użytkownika. Poniżej zamieścilem obrazek pokazujący w jaki sposób środowisko zinterpretuje te parametry.



Dalsza część definicji zdarzenia jest ważniejsza. Przede wszystkim zawiera ona dokładną specyfikację metody czyli nazwę modułu w jakim znajduje sie jej kod (węzeł ModuleSpecificationId), nazwę klasy w jakiej została zdefiniowana (węzeł TypeName), nazwę metody (węzeł MethodName) oraz pełną sygnaturę metody (węzeł MethodId). Jeśli podamy błędne dane nasze zdarzenie nie zadziała dlatego trzeba być uważnym.

Większą swobodę dają nam węzły ShortDescription oraz LongDescription definiujące opis podstawowy/skrócony i rozszerzony zdarzenia. O co dokładnie chodzi? Zostało to pokazane na poniższym obrazku przedstawiającym zdarzenia zarejestrowane przez IntelliTrace w czasie monitorowania programu.



Opis skrócony zdarzenia widoczny jest zawsze, podczas gdy opis rozszerzony dopiero po kliknięciu zdarzenia. Opis te mogą zawierać dowolne napisy, a także odwoływać się do wyniku zwróconego przez metodę, do wartości parametrów wywołania czy też do składowych klasy. O tym jakie informacje są dostępne dla zdarzenia decyduje zawartość węzła DataQueries. Może on zawierać dowolną liczbę podwęzłów DataQuery. Każdy podwęzeł o takiej nazwie definiuje coś w rodzaju zapytania, które zwróci np.: wartość parametru wywołania metody. Aby odwołać się do jakiegoś zapytania używamy składni {index} gdzie index to numer zapytania w numeracji od zero. Znaczenie poszczególnych atrybutów jest następujące.

  • index Przyjmuje odpowiednio wartości: -1 gdy odwołujemy się do wyniku zwracanego przez metodę, 0 gdy odwołujemy się do obiektu na rzecz, którego została wywołana metoda lub do pierwszego parametru jeśli to metoda statyczna, 0,1,2... gdy odwołujemy się do kolejnych parametrów wywołania metody.
  • maxSize Określa maksymalny rozmiar wyniku zwróconego przez zapytanie. Jeśli zostanie przekroczony to wynik zostanie obcięty.
  • type Typ wyniku. Na przykład jeśli pobieramy wartości parametru wywołania metody, który jest typu String to wartość tego atrybutu powinna być równa String.
  • name, _locID, _locAttrData Z mojego doświadczenia wynika, że można tu wpisać co kolwiek. Osobiście jednak wypełniając te atrybuty wzoruję się na innych definicjach zdarzeń.
  • query Atrybut używany kiedy chcemy odwołać się do składowej obiektu. W najprostszej postaci zawiera po prostu nazwę składowej.
Skoro już wiemy jak zbudowana jest definicja zdarzenia umieśćmy ją w pliku CollectionPlan.xml jako dziecko węzła DiagnosticEventSpecifications. Teraz konieczne jest ponowne uruchomienie VS 2010 i już możemy cieszyć się nowym zdarzeniem. Nic trudnego, prawda?

Na koniec ostrzeżenie. Przy dodawaniu nowych zdarzeń należy zachować umiar. Domyślny zestaw zdarzeń został dobrany tak aby nie wpływać na wydajność monitorowanego programu. Jeśli dodamy zdarzenie dla metody wywoływanej bardzo często wpłynie to negatywnie na jego wydajność podczas pracy z IntelliTrace.

W następnym poście na temat IntelliTrace pokażę jak dodać zdarzenie związane z naszą własną metodą, w jaki użyć węzłów DataQuery aby odwołać się do składowej obiektu oraz jak analizować wystąpienia zdarzeń w sposób programistyczny.

07/05/2010

ShowDialog i zwalnianie zasobów

Home

TestForm form = new TestForm();

if (form.ShowDialog() == DialogResult.OK)
{
  ...
}
Czy powyższy króciutki fragment kodu powodujący wyświetlenie okna dialogowego jest poprawny? Niestety, jeszcze do niedawna powiedziałbym bez mrugnięcia oka, że oczywiście tak. Niestety ponieważ ta odpowiedź jest niepoprawna. Do tego aby ten kod był poprawny brakuje niewielkiej ale za to bardzo istotnej rzeczy i zostało to pokazane poniżej:

using(TestForm form = new TestForm())
{
  if (form.ShowDialog() == DialogResult.OK)
  {
    ...
  }
}
Zamiast klauzuli using można oczywiście wywołać Dispose. Dlaczego jednak jawne zwolnienie zasobów jest w ogóle potrzebne? Czy to oznacza, że za każdym razem kiedy chcemy wyświetlić jakieś okno musimy pamiętać o klauzuli using?

Odpowiedź brzmi nie, jest to konieczne tylko jeśli wyświetlamy okno dialogowe. W takim wypadku po jego zamknięciu nie następuje wywołanie metody Close, która zwolni za nas zasoby. Dzięki temu możemy użyć tego samego okna ponownie. Nie jest to żadna wiedza tajemna i można o tym przeczytać w dokumentacji metody ShowDialog:

Unlike modeless forms, the Close method is not called by the .NET Framework when the user clicks the close form button of a dialog box or sets the value of the DialogResult property.

Kolejny raz przekonuję się, że nawet jeśli czegoś używamy bardzo często i jest to dla nas coś prostego i oczywistego to nie znaczy to, że robimy to na 100% dobrze :)