Showing posts with label .NET. Show all posts
Showing posts with label .NET. Show all posts

26/02/2011

Jak zagłodzić Timer?

Home

Okazuje się, że bardzo prosto, ale zacznijmy od początku. Niedawno zakończyłem pracę nad serwerem zajmującym się wykonywaniem tzw. zadań wsadowych. Definicje zadań do wykonania pobierane są z bazy danych, a w danym momencie może działać wiele serwerów. Każdy serwer rezerwuje sobie swoje zadania na pewien kwant czasu. Po upływie tego czasu inne serwery mają prawo przejąć to zadanie. Może się tak zdarzyć na przykład jeśli jakiś serwer ulegnie awarii. Jeśli wykonanie danego zadania zajmuje więcej czasu niż czas rezerwacji to serwer musi przedłużyć dzierżawę.

W tym celu stworzyłem komponent, który monitoruje zadania przetwarzane przez serwer i kiedy zbliża się czas wygaśnięcia dzierżawy, przedłuża ją. Komponent ten korzysta z klasy Timer z przestrzeni nazw System.Timers, która co określony kwant czasu generuje zdarzenie. W metodzie obsługującej to zdarzenia nie robię nic innego jak po prostu aktualizuję czas wygaśnięcia dzierżawy. Tyle w telegraficznym skrócie.

W czasie testowania stworzonego rozwiązania zauważyłem, że w niektórych przypadkach czas wygaśnięcia dzierżawy nie jest aktualizowany. Wyglądało na to jakby klasa Timer nie generowała zdarzeń albo generowała je z dużym opóźnieniem! Wbrew pozorom rozwiązanie zagadki okazało się bardzo proste. Otóż klasa Timer generuje zdarzenie Elapse w wątku pochodzącym z puli wątków ThreadPool. Jeśli dodamy do tego fakt, że zadania wsadowe również są wykonywane przez wątki z puli to wszystko staje się oczywiste. Jeśli serwer umieści w puli odpowiednio dużo zadań wsadowych to może okazać sie, że brakuje wątków dla klasy Timer.

Poniżej prosty kod prezentujący ten efekt. Na początku ograniczam liczbę wątków do 10 i zlecam wykonanie 10 zadań. Zanim wszystkie zadania zostaną uruchomione na ekran zostanie wypisanych kilka napisów Hello World from System.Timers.Timer!. Następnie kiedy wszystkie 10 zadań zostanie uruchomionych napis przestanie się pokazywać. Zobaczymy go ponownie kiedy przynajmniej jedno zadanie zostanie zakończone i tym samym zwolni sie jeden wątek.
class Program
{
  static void Main(string[] args)
  {
    ThreadPool.SetMaxThreads(10, 10);
    
    System.Timers.Timer t = new System.Timers.Timer();
    t.Interval = 1000;
    t.Elapsed += new System.Timers.ElapsedEventHandler(t_Elapsed);
    t.Start();

    for (int i = 0; i < 10; ++i)
        ThreadPool.QueueUserWorkItem((o) =>
            {
              Console.WriteLine("Start " + o);
              Thread.Sleep(10000);
              Console.WriteLine("End " + o);
            }, i);

    Console.ReadLine();
  }

  static void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
  {
    Console.WriteLine("Hello World from System.Timers.Timer!");
  }
}
Taki sam efekt otrzymamy jeśli użyjemy klasy System.Threading.Timer zamiast System.Timers.Timer. Jak można poradzić sobie z tym problemem. Uważam, że są trzy rozwiązania:
  • Zliczać liczbę zadań umieszczanych w puli wątków i pilnować aby zawsze, przynajmniej jeden wątek z puli był wolny i mógł być użyty przez klasę Timer. Maksymalną liczbę wątków możemy pobrać przy pomocy metody GetMaxThreads. Jest to jednak podejście sprzeczne z ideą puli wątków, do której wrzucamy zadania do wykonania i nie zastanawiamy się kiedy dokładnie zostanie uruchomione, przez jaki wątek itd.
  • Zrezygnować z klasy ThreadPool i samemu zarządzać wątkami.
  • Użyć (napisać) zegar odpalający zdarzenia Elapse poza pulą wątków.
W opisie problemu pominąłem Task Parallel Library ponieważ w omawianym przypadku użycie .NET 4.0 nie było możliwe. Szybki test pokazał jednak, że jeśli mógłbym użyć Task Parallel Library to problem nie wystąpiłby. Z tego co wiem Task Parallel Library nie używa klasy ThreadPool, a więc przyczyna problemu nie występuje.

16/02/2011

DockPanel i ScrollViewer

Home

Dlaczego lubię kontener DockPanel? Ponieważ dobrze (intuicyjnie) współpracuje z kontrolką ScrollViewer, która dostarcza pionowych i poziomych pasków przewijania. Załóżmy, że główne okno naszej aplikacji zawiera listę (kontrolka ListView) z kilkuset wierszami. Wierszy jest na tyle dużo, że w danym momencie na ekranie widoczna jest tylko część z nich. Aby użytkownik mógł przewijać listę i zobaczyć wszystkie wiersze używamy kontrolki ScrollViewer

ScrollViewer zachowa się jednak inaczej w zależności od kontenera w jakim zostanie osadzony. Jeśli będzie to np.: StackPanel lub Grid to paski przewijania nie zostaną wyświetlone i otrzymamy efekt jak poniżej.



Można sobie z tym poradzić ustawiając na kontrolce ScrollViewer właściwość Height ale spowoduje to, że lista będzie miała zawsze tą samą wysokość bez względu na wielkość okna. Podobnie można ustawić właściwość Height dla listy ale efekt będzie taki sam.



Dużo lepszym rozwiązaniem jest osadzenia kontrolki ScrollViewer wewnątrz kontenera DockPanel. Dzięki temu uzyskamy efekt jak poniżej. Paski przewijania są widoczne, a wysokość listy zależy od wielkości okna.


09/11/2010

Poznaj swój program

Home

Tematykę IntelliTrace poruszałem już kilkukrotnie. Dzisiaj chciałbym powrócić do zagadnienia opisanego w poście Logi IntelliTrace bez tajemnic czyli analizy logu przy pomocy IntelliTrace API. Tym razem napiszę w jaki sposób dostać się do informacji o tym kiedy została wywołana jakaś metoda, jaki wynik zwróciła i jakie były parametry wywołania. Informacje te są prezentowane w oknie Calls View w Visual Studio 2010 ale można je analizować w ograniczonym stopniu. Poniższe informacje przydadzą się każdemu kto będzie chciał na przykład załadować te dane do bazy danych w celu później analizy, na przykład przy pomocy algorytmów odkrywania wiedzy (data mining).

Zacznijmy od wzorca kodu, który posłuży nam do odczytania pliku z logiem:
using (IntelliTraceFile file = new IntelliTraceFile(pathToIntelliTraceLog))
{
  //Kolekcja procesów będzie miała tylko jedną pozycję
  foreach (IntelliTraceProcess process in traceFile.Processes)
  {
    //Przetworzenie kolejnych wątków
    foreach (IntelliTraceThread thread in process.Threads)
    {
      //Tworzymy strumień ze zdarzeniami. Każde zdarzenie odpowiada np.: wywołaniu metody
      Chain chain = thread.CreateThreadChain<ThreadChain>();
  
      EventToken eventToken = chain.FirstValidToken;
      //Przetwarzamy zdarzenie po zdarzeniu
      while (eventToken != chain.AfterLastToken)
      {
        //Pobranie zdarzenia
        IntelliTraceEvent ev = chain.GetEvent(eventToken);

        //To zdarzenie reprezentujące wywołanie metody
        if(ev is MethodEnterEvent)
          ProcessMethodEnterEvent(ev);
        //To zdarzenie reprezentujące zakończenie wywołania metody
        else if(ev is MethodExitEvent)
          ProcessMethodExitEvent(ev);
          
        eventToken = chain.GetNextToken(eventToken);
      }
    }
  }
}
Powyższy kod zawiera dużo komentarzy dlatego nie powinno być trudności z jego zrozumieniem. Chciałbym zwrócić uwagę tylko na jedną rzecz. Lista zdarzeń jakie możemy obsłużyć zawiera znacznie więcej pozycji niż tylko dwie pokazane powyżej (MethodEnterEvent, MethodExitEvent). Niestety ale nie ma dobrej dokumentacji z pełną listą jaki "łańcuch zdarzeń" jakie zdarzenia udostępnia. Pozostaje droga eksperymentalna oraz przyjrzenie się projektowi iTraceReader, o którym już pisałem w poście Logi IntelliTrace bez tajemnic.

Zobaczmy, więc jak wyglądają metody ProcessMethodEnterEvent oraz ProcessMethodExitEvent. Na samym początku należy zamienić zdarzenia MethodEnterEvent oraz MethodExitEvent, które zawierają tylko suchy ciąg bajtów na bardziej przyjazne ResolvedMethodEnterEvent oraz ResolvedMethodExitEvent, z których łatwo wyciągniemy interesujące nas informacje.
private void ProcessMethodEnterEvent(IntelliTraceEvent ev)
{
  ResolvedMethodEnterEvent methodEnterEvent = new ResolvedMethodEnterEvent(CurrentProcess, ev as MethodEnterEvent);
  //...
}
Klasy ResolvedMethodEnterEvent oraz ResolvedMethodExitEvent zawierają właściwość Method, która zwraca obiekt klasy ResolvedMethod. Klasa ta reprezentuje wywołaną metodę i udostępnia następujące bardzo intuicyjne właściwości:
  • ContainingTypeName - Pełna nazwa typu (razem z przestrzenią nazw) w ramach, którego metoda została zdefiniowana.
  • MethodName - Nazwa metody.
  • ParameterNames - Nazwy (ale nie wartości) parametrów.
  • ParameterTypeNames - Pełne nazwy typów parametrów.
  • ReturnTypeName - Pełna nazwa typu (ale nie wartość) zwracanego przez metodę.
Teraz przejdźmy do odczytania parametrów przekazanych do metody przy jej wywołaniu. Do tego celu posłuży nam metoda:

IList<IDataElement> ResolvedMethodEnterEvent.GetParameters()

W celu reprezentowania różnych bytów IntelliTrace API udostępnia interfejs o nazwie IDataElement. IDataElement może reprezentować typy proste i referencyjne. W przypadku typu referencyjnego IDataElement może zawierać elementy podrzędne reprezentujące składowe obiektu, które również będą reprezentowane przez IDataElement. Napisałem może, a nie musi ponieważ IntelliTrace analizuje tylko obiekty pierwszego poziomu. Czyli jeśli parametr jakiejś metody to obiekt klasy A, który zawiera składowe typu string, int oraz wskazanie na obiekt klasy B to IntelliTrace zapisze w logu wartości wszystkich składowych typów prostych obiektu klasy A ale już nie obiektu klasy B. Na temat obiektu klasy B dowiemy się tylko tyle, że jest. Poniżej zamieszczam opis interfejsu IDataElement. Zaznaczam jednak, że tworząc ten opis bazowałem na doświadczeniu i przeprowadzonych eksperymentach dlatego może on zawierać nieścisłości lub błędy:
  • HasChildren - true jeśli mamy do czynienia z typem referencyjnym i jest to obiekt pierwszego poziomu, false w przypadku typów prostych i obiektów zagnieżdżonych.
  • Name - Nazwa parametru, nazwa składowej obiektu, '[Return value]' dla wartości zwracanej przez metodę, this lub base.
  • TypeName - Pełna nazwa typu reprezentowanego bytu.
  • Value - W przypadku typów prostych właściwa wartość, a w przypadku typów referencyjnych nazwę typu. Jeśli wartość nie została zapisana przez IntelliTrace otrzymamy wynik 'unknown'.
  • GetChildren - Metoda, którą możemy wywołać jeśli właściwość HasChildren jest równa true. Metoda ta zwraca elementy podrzędne w stosunku do bieżącego.
Warto również wiedzieć o poniższej metodzie, która służy do pobrania wyniku zwróconego przez metodę:

IDataElement ResolvedMethodExitEvent.GetReturnValue()

Znacznie rzadziej będą nas interesowały parametry typu out, do których uzyskamy dostęp przy pomocy poniżej metody. Niestety ale wygląda na to, że metoda ta zawiera jakiś błąd. Próbowałem wywołać ją kilkukrotnie dla rożnych metod z parametrem out typu string. Za każdym razem kończyło się to błędem. Raz był to wyjątek ArgumentOutOfRangeException innym razem ApplicationException.

IList<IDataElement> ResolvedMethodExitEvent.GetOutputs()

Podsumowując pomimo ograniczeń IntelliTrace API zachęcam do zabawy z logami.

28/10/2010

Znikające Activity

Home

Na poniższym obrazku widać dwa schematy pochodzące z rożnych aplikacji, a przedstawiające fragment pewnego procesu biznesowego. Z prawej strony można zobaczyć definicję tego procesu, a z lewej powołaną do życia i uruchomioną jego instancję.



Niestety ale z jakichś powodów instancja różni się od swojej definicji. Pytanie dlaczego? Początkowo myślałem, że to jakiś problem z zapisywaniem lub odczytywaniem definicji/instancji procesu z bazy danych. Pomysł ten jednak szybko odrzuciłem ponieważ gdyby problem tkwił w tym miejscu to odczyt z bazy danych, deserializacja obiektu i w końcu wyświetlenie definicji/instancji procesu w standardowej kontrolce WorkflowView nie udało by się.

W dalszej kolejności zacząłem szukać różnic pomiędzy wspomnianymi aplikacji. Szybko udało się wykryć (dziękuję za pomoc Tomkowi), że kod brakującego Activity znajduje się w bibliotece niedostępnej dla aplikacji. Po skopiowaniu odpowiedniej dll'ki do katalogu z binariami aplikacji problem ustąpił.

Zacząłem się jednak zastanawiać czemu nie został zgłoszony żaden wyjątek informujący o braku dll'ki. Od razu pomyślałem, że ktoś musiał go przechwycić i zgasić. W sumie to się nie pomyliłem. Po włączeniu Managed Debugging Assistant dla wszystkich wyjątków (Debug -> Exceptions) i ponownym uruchomieniu aplikacji asystent pokazał, że wyjątek pojawia się w czasie deserializowania instancji procesu, gdzieś w środku klasy ActivityMarkupSerializer. Pojawienie się wyjątku nie przeszkadza jednak dokończyć deserializacji i zwrócić obiekt reprezentujący proces tylko, że bez kilku Activity.

Można powiedzieć, że ActivityMarkupSerializer jest bardzo sprytny ponieważ potrafi sobie poradzić nawet jeśli brakuje jakiejś biblioteki. Moim zdaniem ta klasa jest jednak za sprytna. Teraz już będę o tym wiedział ale na takim błędzie można stracić mnóstwo czasu.

09/08/2010

Logi IntelliTrace bez tajemnic

Home

IntelliTrace to jedno z najciekawszych narzędzi jakie pojawiło się w Visual Studio 2010. Dla tych, którzy jeszcze go nie znają w skrócie służy do nagrywania działania programu w celu późniejszej jego analizy. Nagrywane są wywołania metod oraz tzw. zdarzenia diagostyczne czyli ważne punkty w historii działania programu np.: nawiązanie połączenia z bazą danych, wykonanie zapytania do bazy danych, załadowanie modułu czy uruchomienie wątku. Visual Studio 2010 udostępnia kilka sposobów ich analizowania.

Listę wszystkich zarejestrowanych zdarzeń znajdziemy w oknie IntelliTrace Events View. Możemy je przefiltrować na podstawie wątku w jakim wystąpiły lub kategorii do jakiej należą. W oknie Calls View zobaczymy natomiast zdarzenia wplecione pomiędzy wywołania metod. Na tej podstawie łatwo zorientować się co doprowadziło do ich wygenerowania. Możliwości są więc całkiem spore ale jednak ograniczone. Z pomocą przychodzi IntelliTrace API, które pozwala na programową analizę logów IntelliTrace (*.iTrace).

Do czego może się to przydać? Do głowy przychodzi mi kilka pomysłów. Pierwszy z brzegu to program to graficznej wizualizacji zdarzeń. Dalej analiza logu pod kątem poprawności działania programu na podstawie analizy częstotliwości występowania poszczególnych typów zdarzeń. Sądzę, że w podobny sposób można analizować wydajność aplikacji. Może się również zdarzyć, że dane prezentowane przez Visual Studio będą dla nas zbyt ubogie i jedynym wyjściem będzie napisanie własnego analizatora.

Aby skorzystać z interfejsu programistycznego IntelliTrace powinniśmy zacząć od dodania do projektu referencji do biblioteki Microsoft.VisualStudio.IntelliTrace.dll. Znajdziemy ją w standardowym oknie Add Reference w zakładce .NET. Właściwą analizę logu rozpoczynamy od jego wczytania przy pomocy klasy IntelliTraceFile.
using (IntelliTraceFile file = new IntelliTraceFile(pathToIntelliTraceLog))
{
...
}
Klasa ta ma tylko jedną interesującą właściwość o nazwie Processes. Przy jej pomocy uzyskujemy dostęp do listy procesów, dla których log zawiera jakieś zdarzenia. W praktyce lista ta będzie zawierać tylko jedną pozycję. Proces reprezentowany jest przez klasę IntelliTraceProcess, która jest bardziej interesująca. Dzięki niej możemy dowiedzieć się o modułach załadowanych do procesu (właściwość Modules), uruchomionych wątkach (właściwość Threads) czy środowisku w jakim został uruchomiony proces (właściwość SystemInformationEvent). Wątki reprezentowane są przez klasę IntelliTraceThread.

Najważniejsze jest to, że klasy IntelliTraceProcess oraz IntelliTraceThread pozwalają na dobranie się do zdarzeń IntelliTrace. Służą do tego odpowiednio metody IntelliTraceProcess.CreateProcessChain oraz IntelliTraceThread.CreateThreadChain. Pierwszej z nich użyjemy jeśli interesują nas "łańcuchy zdarzeń" globalne dla całego procesu, a drugiej jeśli "łańcuchy zdarzeń" specyficzne dla danego wątku. Druga kategoria jest mniejsza i zawiera tylko trzy pozycje: ThreadChain, ThreadStreamChain oraz ThreadCheckpointChain. Warto zaznaczyć, że na tym poziomie wszystko jest zdarzeniem czyli zarówno wspomniane zdarzenia diagnostyczne jak i wywołania metod będą obsługiwane w taki sam sposób. Różnica polega tylko na innej klasie zdarzenia. Poniżej przykład wywołania metody IntelliTraceThread.CreateThreadChain, które zwróci łańcuch ze zdarzeniami odpowiadającymi min.: wywołaniu metod:
Chain chain = thread.CreateThreadChain<ThreadChain>();
Teraz nie pozostaje nic innego jak odczytać i przetworzyć kolejne zdarzenia:
EventToken eventToken = chain.FirstValidToken;
while (eventToken != chain.AfterLastToken)
{
  IntelliTraceEvent ev = chain.GetEvent(eventToken);

  //...

  eventToken = chain.GetNextToken(eventToken);
}
Klasa IntelliTraceEvent jest klasą bazową dla wszystkich typów zdarzeń, a więc aby uzyskać dokładniejsze dane należy wykonać rzutowanie na jeden z wydziedziczonych z niej typów.

Warto zwrócić uwagę na jedną szczególnie interesująca klasę z perspektywy analizy logu. Mam tutaj na myśli klasę DiagnosticStreamChain przy pomocy, której uzyskamy dostęp do tzw. zdarzeń diagnostycznych. Przykładem zdarzenia diagnostycznego jest wspomniane wcześniej wykonanie zapytania do bazy danych, naciśnięcie przycisku, wypisanie czegoś na konsolę czy przeładowanie strony w aplikacji ASP.Net. Zdarzeń tych jest dużo, a pełną ich listę znajdziemy w ustawieniach Visual Studio: Tools->Options->IntelliTrace->IntelliTrace Events. Możliwość analizy tego typu zdarzeń jest tym ciekawsza jeśli uwzględnimy możliwość rozszerzania tej listy o swoje własne zdarzenia! Zainteresowanych odsyłam do moich dwóch wcześniejszych tekstów Własne zdarzenia IntelliTrace oraz Własne zdarzenia IntelliTrace 2 .

Użycie IntelliTrace API nie jest skomplikowane ale nasuwa się pytanie:

Gdzie znaleźć dokumentację, szczegółowy opis poszczególnych klas reprezentujących zdarzenia, "łańcuchy zdarzeń" itd.?

Niestety ale dostępna dokumentacja na ten temat jest bardzo uboga. Znajdziemy w niej co prawda listę typów zdarzeń czy "łańcuchów" ale bez jakiegokolwiek opisu. Chyba, że opisem można nazwać coś takiego: "Describes a DiagnosticEvent." (dla klasy DiagnosticEvent) albo coś takiego: "Describes the ThreadCreateEvent." (dla klasy ThreadCreateEvent). Dla mnie to masło maślane. Konie z rzędem temu kto domyśli się na podstawie dostępnych informacji czym na przykład różnią się klasy ThreadChain oraz ThreadStreamChain? Można do tego dojść analizując kod biblioteki przy pomocy reflektora lub eksperymentalnie ale wymaga to dodatkowego wysiłku.

W zgłębianiu IntelliTrace API pomocny okaże się natomiast bardzo fajny projekt o nazwie iTraceReader, który znajdziemy tutaj. iTraceReader to nakładka na surowe IntelliTrace API, która ułatwia wykonanie wielu czynności. Zapoznając się z tym projektem można nauczyć się bardzo wielu rzeczy. Wszystkich, których ten temat zaciekawił zachęcam do bliższego przyjrzenia się.

28/07/2010

Używanie IntelliTrace poza Visual Studio 2010!

Home

W oficjalnej dokumentacji IntelliTrace można przeczytać, że narzędzie to jest dostępne tylko i wyłącznie z poziomu środowiska Visual Studio 2010. To bardzo źle ponieważ z góry przekreśla użycie IntelliTrace do diagnozowania błędów u klienta. Wyobraźmy sobie, że dostajemy zgłoszenie trudnego do powtórzenia błędu. Czy nie byłoby wspaniale uruchomić aplikację w środowisku produkcyjnym, pod kontrolą IntelliTrace i w razie wystąpienia błędu poprosić klienta o przekazanie nam plików z logami (*.iTrace). Nie wszystko jest jednak stracone. Pomimo, że nie jest to oficjalnie wspierane można uruchomić IntelliTrace z poza Visual Studio 2010!

Na początek należy odpowiedzieć na pytanie czym jest IntelliTrace: wątkiem działającym w ramach procesu Visual Studio 2010, niezależnym procesem, a może jeszcze czymś innym? Odpowiedź na to pytanie można uzyskać przy pomocy programu Process Explorer. Na poniższym obrazku widać fragment drzewa procesów dla węzła devenv.exe (czyli dla Visual Studio).



Pod nim znajdziemy węzeł odpowiadający debug'owanemu programowi oraz węzeł IntelliTrace, a w jego właściwościach ścieżkę z jakiej jest uruchamiany (VS_2010_INSTALL_DIR to katalog instalacyjny środowiska) np.:

VS_2010_INSTALL_DIR\Team Tools\TraceDebugger Tools\IntelliTrace.EXE

Reasumując IntelliTrace to niezależny proces, który komunikuje się z Visual Studio przy użyciu jakiegoś mechanizmu IPC (ang. Inter-process communication ). Jeśli spróbujemy go uruchomić z linii poleceń na ekran zostanie wypisana lista dostępnych komend. Z kilku dostępnych interesująca jest komenda launch, która zgodnie z podanym opisem uruchamia podaną aplikację i rozpoczyna jej monitorowanie. Po wpisaniu w linię poleceń: IntelliTrace help launch uzyskamy bardziej szczegółowe informacje na jej temat.

Z dostępnych parametrów najważniejsze to /logfile (w skrócie /f) przy pomocy, którego wskazujemy docelowy plik z logiem oraz /collectionplan (w skrócie /cp) przy pomocy, którego wskazujemy plik z planem działania, konfiguracją IntelliTrace. Skąd go wziąć? Domyślny plik używany przez Visual Studio 2010, a który jest dokumentem XML, znajdziemy w lokalizacji:

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

Mając już wszystkie elementy układanki, spróbujmy uruchomić jakiś program pod kontrolą IntelliTrace ale bez pomocy Visual Studio. Poniżej pokazano takie przykładowe wywołanie. W miejsce LOG_PATH należy wstawić ścieżkę do wyjściowego pliku z logiem (najlepiej nadać mu rozszerzenie iTrace), w miejsce COLLECTION_PLAN_PATH ścieżkę do pliku CollectionPlan.xml (może nazywać się inaczej i mieć inne rozszerzenie). PROGRAM_PATH to oczywiście ścieżka do programu jaki chcemy monitorować.

VS_2010_INSTALL_DIR\Team Tools\TraceDebugger Tools\IntelliTrace.EXE launch /f:LOG_PATH /cp:COLLECTION_PLAN_PATH PROGRAM_PATH

Po wykonaniu tego polecenia we wskazanym przez nas katalogu pojawi się plik z logiem. Jeśli będzie miał rozszerzenie iTrace to po dwukliku uruchomi się Visual Studio 2010. W tym momencie czeka nas jednak niemiła niespodzianka ponieważ okaże się, że plik z logiem nie zawiera żadnych informacji tak jakby program nie był monitorowany.



Na brakujący element układanki naprowadzi nas ponownie Process Explorer, który umożliwia podejrzenie parametrów wywołania programu. Interesuje nas oczywiście proces IntelliTrace. Jeśli odczytamy jego parametry wywołania to dowiemy się, że został uruchomiony z komendą run, a nie launch. Różnica polega na tym, że run w przeciwieństwie do launch uruchamia tylko proces monitorujący i nie wskazuje konkretnej aplikacji do monitorowania. To jaka aplikacja będzie monitorowana zależy od Visual Studio 2010. Kolejna, ważna dla nas różnica to wartość parametru /cp, który nie wskazuje na domyślny plan wykonania ale na jakiś "dziwny" plik np.:

C:\Users\user\AppData\Local\Microsoft\VisualStudio\10.0\TraceDebugger\Settings\frueouq2.vfs

Jest to plik tymczasowy, a jego nazwa generowana jest w sposób losowy. Jeśli spróbujemy go otworzyć to okaże się, że bardzo przypomina domyślny plik CollectionPlan.xml. Dokładniej mówiąc schemat XML jest taki sam, inne są natomiast wartości atrybutów, liczba węzłów itd. czyżby brakujący element układanki? Tworzymy, więc kopię tego pliku, wskazujemy go w parametrze \cp i ponownie wydajemy wcześniej pokazane polecenie. Tym razem po otworzeniu wynikowego pliku z logiem zobaczymy listę monitorowanych wątków, drzewo wywołań itd.



Pełny sukces? Niestety nie do końca. Po bliższym przyjrzeniu się zauważymy, że mamy dostęp tylko do drzewa wywołań, a brakuje informacji o zdarzeniach diagnostycznych (ważne punkty w historii wykonania programu np.: wykonanie zapytania do bazy danych). Tego ograniczenia nie udało mi się jeszcze ominąć. Przypuszczam, że przy uruchamianiu IntelliTrace przez Visual Studio 2010 do programu przekazywane są jakieś dodatkowe opcje. Być może do rejestrowania zdarzeń potrzebne jest Visual Studio, a logger IntelliTrace nie potrafi tego robić?

Najważniejsze jest jednak to, że już teraz IntelliTrace można uruchomić niezależnie od środowiska programistycznego, chociaż z pewnymi ograniczeniami. Biorę to za dobrą monetę i mam nadzieję, że pełne wsparcie dla uruchamiania IntelliTrace z poza Visual Studio jest w planach Microsoft'u i pojawi się, jeśli nie z którymś service pack'iem to w kolejnej edycji Visual Studio.

Artukuł opublikowałem również w serwisie software.com.pl.

16/06/2010

Własne zdarzenia IntelliTrace 2

Home

Dzisiaj, zgodnie z wcześniejszą obietnicą, chciałem pokazać w jaki sposób zdefiniować nowe zdarzenie diagnostyczne dla naszej własnej metody. W tym celu użyję bardzo prostej klasy pokazanej poniżej:

namespace SmallTest
{
    public class Fun
    {
        public void Hello(int count, string msg)
        {
            for (int i = 0; i < count; ++i)
                Console.WriteLine(msg);
        }
    }
}

Przyjmijmy, że klasa ta znajduje się w projekcie o nazwie SmallTest po skompilowaniu, którego powstanie plik SmallTest.dll. Skoro mamy już z czym pracować przystąpmy do modyfikacji pliku ColllectionPlan.xml. Cały proces będzie bardzo podobny do tego co pokazałem w poprzednim poście. Zaczynamy od znalezienia węzła o nazwie ModuleList i umieszczamy pod nim węzeł wskazujący na naszą bibliotekę:

  <ModuleSpecification Id="SmallTest">SmallTest.dll</ModuleSpecification>

Co istotne biblioteka nie musi być podpisana, ani znajdować się w jakimś konkretnym katalogu. Następnie tworzymy nową kategorię dla zdarzeń. W tym celu pod węzłem Categories powinniśmy umieścić pokazany poniżej węzeł XML. Jeśli nie chcemy tworzyć nowej kategorii możemy pominąć ten krok.

  <Category Id="my" _locID="category.My">My</Category>

Na koniec najważniejsza rzecz, zdefiniowanie nowego zdarzenia. Postępujemy dokładnie tak samo jak przy definiowaniu zdarzenia dla metody platformy .NET. Potrzebny kod XML został pokazany poniżej. Umieszczamy go pod węzłem DiagnosticEventSpecifications. Jeśli w poprzednim kroku nie tworzyliśmy nowej kategorii wartość węzła CategoryId powinna odpowiadać nazwie innej, już istniejącej kategorii.

    <DiagnosticEventSpecifications>
      <DiagnosticEventSpecification>
        <CategoryId>my</CategoryId>
        <SettingsName _locID="settingsName.Fun.Hello">Fun.Hello</SettingsName>
        <SettingsDescription _locID="settingsDescription.Fun.Hello">Fun.Hello</SettingsDescription>
        <Bindings>
          <Binding>
            <ModuleSpecificationId>SmallTest</ModuleSpecificationId>
            <TypeName>SmallTest.Fun</TypeName>
            <MethodName>Hello</MethodName>
            <MethodId>SmallTest.Fun.Hello(System.Int32, System.String):System.Void</MethodId>
            <ShortDescription _locID="shortDescription.Fun.Hello">Fun.Hello({0},{1})</ShortDescription>
            <LongDescription _locID="longDescription.Fun.Hello">Fun.Hello(count={0},msg={1})</LongDescription>
            <DataQueries>
              <DataQuery index="1" maxSize="0" type="Int32" _locID="dataquery.Fun.Hello.count" _locAttrData="name" name="count" query="" />
              <DataQuery index="2" maxSize="256" type="String" _locID="dataquery.Fun.Hello.msg" _locAttrData="name" name="msg"  query=""></DataQuery>
            </DataQueries>
          </Binding>
        </Bindings>
      </DiagnosticEventSpecification>

Szczegółowy opis znaczenia poszczególnych węzłów XML użytych w tej definicji podałem w poprzednim poście, a więc nie będę go tutaj powielał. Uruchamiany Visual Studio i jeśli nie popełniliśmy żadnego błędu możemy już korzystać z nowego zdarzenia. W celu sprawdzenia czy wszystko jest w porządku najpierw zaglądamy do Tools->Options->IntteliTrace->IntteliTrace Events. Powinniśmy zobaczyć nową kategorię, a po jej rozwinięciu nowe zdarzenie:



Teraz możemy stworzyć nowy projekt konsolowy i dodać referencję to biblioteki SmallTest. Kiedy to zrobimy, nie pozostaje nic innego jak utworzyć obiekt klasy Fun i wywołać metodę Hello:

            Fun f = new Fun();
            f.Hello(2, "Hello world!");

Po skompilowaniu i uruchomieniu programu pod kontrolą debugger'a i z włączonym mechanizmem IntteliTrace w oknie z zarejestrowanymi zdarzeniami zobaczymy nasze zdarzenie:



W następnym poście pokażę w jaki sposób analizować wystąpienia zdarzeń w sposób programowy.

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 :)

10/04/2010

.NET Reflector Pro

Home

.NET Reflector Pro to najnowsza odsłona dobrze znanego programistą .NET narzędzia, firmy redgate, ktora pojawiła się na rynku 15 lutego. Do tej pory nie miałem okazji się z nią zapoznać ale ostatnio nadrobiłem zaległości i jestem zachwycony nowymi możliwościami programu. Co mi się tak spodobało?

Otóż .NET Reflector Pro to plugin integrujący się z środowiskiem Visual Studio, który pozwala na dekompilację pakietów C#/VB w locie, a następnie ich debugowanie! Innymi słowy jeśli korzystamy z bibliotek jakiegoś zewnętrznego dostawcy ale nie posiadamy jej źródeł, a z jakiegoś powodu chcemy ją zdebugować postępujemy następująco:
  • Wybieramy polecenie Choose Assemblies To Debug z menu .NET Reflector w Visual Studio.
  • Zaznaczamy interesujące nas pakiety.
  • Klikamy OK.
  • Rozpoczynamy debugowanie, a niewidoczny do tej pory kody stoi dla nas otworem. Możemy stawiać pułapki, podglądać wartości zmiennych itd.
Proste, prawda? Bardzo lubię takie narzędzia, minimum wysiłku i maksimum korzyści. Niestety ale plugin jest płatny - można jednak testować go przez okres 14 dni (do pobrania tutaj). Szczerze polecam!

18/02/2010

Enterprise Library, Informix i CLOB

Home

Data Access Application Block to część Enterprise Library oparta o ADO.NET odpowiedzialna za komunikację z bazą danych. Jedną z bardzo miłych cech tego modułu jest to, że w dużym stopniu ukrywa przed programistą rodzaj bazy danych z jakim pracuje. Uprasza to pisanie aplikacji kompatybilnych z wieloma bazami danych.

Oczywiście nie wszystko jest takie różowe o czym przekonałem się ostatnio. W tej chwili pracuję głównie z bazą danych Informix firmy IBM i Data Access Application Block radził sobie z nią całkiem dobrze ale do czasu kiedy postanowiłem wykorzystać typ bazodanowy CLOB służący do przechowywania dużych obiektów znakowych.

Nie wnikając w szczegóły zapis danych do kolumny o typie CLOB nie udał mi się w ogóle, a odczyt częściowo :) Częściowo ponieważ mogłem odczytać tylko fragment danych. Nie wykluczam możliwości, że mogłem popełnić jakiś błąd ale nie chcąc tracić czasu problem rozwiązałem wykorzystując sterownik ADO.NET firmy IBM przeznaczony tylko do obsługi Informix'a. Biblioteka ze sterownikiem nazywa sie IBM.Data.Informix.dll i instalowana jest razem z Client SDK. U mnie znajdowała się w lokalizacji C:\Program Files (x86)\Informix\Client-SDK\bin\netf20. Model pracy z klasami z tej biblioteki jest prawie taki sam jak z standardowymi klasami ADO.NET. Mamy więc klasę IfxCommand, która dziedziczy z DbCommand, IfxConnecton, która implementuje interfejs IDbConnection itd. Różnice są niewielkie i dotyczą rzeczy specyficznych dla Informix'a czyli np.: obsługi typu CLOB.

Wykorzystanie biblioteki rozwiązało oczywiście mój problem. Sądzę, że wybór IBM.Data.Informix.dll jest dobrym pomysłem dla każdego kto ma zamiar pracować z bazą danych firmy IBM. Nie ma jednak róży bez kolców. Użycie rozwiązania właściwego dla konkretnej bazy danych zamiast Enterprise Library wpływa negatywnie na przenoszalność aplikacji, a własciwie ją uniemożliwia. Oczywiście klasy specyficzne dla Informix'a można odpowiednio przykryć tak aby ewentualna podmiana była łatwiejsza ale nie jest to równoznaczne z użyciem Data Access Application Block.

Reasumując jeśli chcę zachować przenaszalnośc aplikacji będę musiał zastanowić się jak dostosować tą bibliotekę do współpracy z Enterprise Library.

31/01/2010

"Decimal byte array constructor requires an array of length four containing valid decimal bytes"

Home

"Decimal byte array constructor requires an array of length four containing valid decimal bytes"

Z takim błędem spotkałem się ostatnio pisząc kod komunikujący się z bazą danych. Niestety ale błąd ten miał również dwie utrudniające jego poprawienie właściwości. Po pierwsze pojawiał się na kilku komputerach ale nie na moim. No cóż syndrom "a u mnie działa" zdarza się każdemu. Po drugie kod w jakim występował zdawał się wyglądać całkowicie niewinnie:
DataSet ds = New DataSet

OleDbDataAdapter adapter = New OleDbDataAdapter
adapter.SelectCommand = New OleDbCommand(Command, Connection)

adapter.Fill(ds)
Dokładniej mówiąc wyjątek pojawiał sie przy wywołaniu metody Fill. Kiedy zapytałem wyszukiwarkę o treść komunikatu znalazłem kilkanaście forów z pytaniami dotyczącymi tego błędy w kontekście komunikacji z różnymi bazami danych (ja używam bazy danych Informix). Niestety prawie wszystkie zadane pytania zostały bez odpowiedzi. Znalazłem jednak wypowiedź sugerującą, że błąd pojawia się jeśli w zapytaniu użyto funkcji agregujących.

Zapytanie, którego ja użyłem zawierało całkiem sporo przykładów użycia funkcji agregujących, postanowiłem więc usunąć je z zapytania i zobaczyć co się stanie. W wyniku modyfikacji zapytania wyjątek nie pojawił się. Miałem więc już jakąś wskazówkę co robić dalej. W następnym kroku dodawałem do zapytania kolejne odwołania do funkcji agregujących żeby zobaczyć, które dokładnie spowoduje błąd.

Po kilku próbach znalazłem kłopotliwy fragment zapytania. Wyglądał on tak:
SELECT ..., SUM(Col1) - Sum(Col2), ... 
W tym momencie przypomniałem sobie pewną rzecz i już wiedziałem jak rozwiązać problem. Tak jak napisałem wcześniej błąd nie pojawiał się na komputerze, na którym pracowałem. Dane, które pobrałem z bazy danych mogłem, więc wyświetlić na kontrolce gridowej. Kiedy zrobiłem to po raz pierwszy zauważyłem, że w niektórych kolumnach liczby prezentowane są z bardzo dużą precyzję pomimo, że na poziomie bazy danych użyto typu DECIMAL o precyzji 2. Problem rozwiązałem wymuszając po prostu odpowiednie formatowanie. Teraz jednak okazało się, że kolumny o "dziwnym" formatowaniu na mojej maszynie pokrywały się z przykładami użycia funckji, które powodowały błąd na innych stacjach.

Pierwszy wniosek jest taki, że wynik operacji arytmetycznych na rezultatach działania funkcji agregujących może mieć wyższą precyzję niż by to wynikało z typu kolumn. W przypadku niektórych działań arytmetycznych ma to oczywiście sens, chociaż nie w przypadku odejmowania czy dodawania. Ale mniejsza o tym. Istotne jest to, że z powodu zwiększonej precyzji nie jest potem możliwe skonstruowanie wartości typu decimal i generowany jest wyjątek. Rozwiązanie jest proste. Trzeba zaokrąglić wynik:
SELECT ..., ROUND(SUM(COl1) - Sum(Col2), 2), ... 
Z tego co się dowiedziałem pomóc może również użycie nowszych sterowników OLEDB. Z drugiej strony na moim komputerze zainstalowana jest taka sama wersja sterowników jak na innych. Różnica jest taka, że ja używam Windows 7 ale trudno powiedzieć czy to ma jakiś wpływ. W każdym razie pytanie czemu błąd nie pojawia się na wszystkich stacjach pozostaje otwarte.

06/01/2010

Uruchamianie aplikacji na 64 bitach

Home

Pierwszy wpis w nowym roku 2010 będzie dotyczył zagadnienia uruchamiania aplikacji .Net'owych na maszynach 64 bitowy. W większości przypadków nie ma z tym żadnego problemu. W końcu IL jest przenośny. Problemy zaczynają się kiedy chcemy wykorzystać w swojej aplikacji biblioteki 32 bitowe. Niestety ale nie jest możliwe aby proces 64 bitowy używał bibliotek 32 bitowych i na odwrót. Przeważnie nie mamy również możliwości przekompilowania biblioteki, z której korzystamy. Ja spotkałem się z tym problem uruchamiając aplikację, która do połączenia z bazą danych korzystała ze sterownika ODBC. W czasie jej uruchomienia pojawiał się komunikat:

Dostawca 'Ifxoledbc.2' nie jest zarejestrowany na lokalnym komputerze.

Komunikat mnie zdziwił ponieważ byłem pewny, że dostawca jest zarejestrowany. Przypomniałem sobie jednak, że jest to sterownik 32 bitowy. Jak sobie poradziłem?

Ogólnie są dwa rozwiązania tego problemu. Po pierwsze można w właściwościach projektu ustawić opcję Target CPU na X86. Domyślna wartość to Any CPU. Jeśli z jakichś powodów nie możemy zrekompilować aplikacji możemy skorzystać z programu corflags. Jest on instalowany razem z Visual Studio i pozwala przełączyć aplikację w 32 bitowy tryb pracy np.:

corflags /32bit+ aplikacja

W razie potrzeby ponownego przełączenia na 64 bitowy tryb pracy używamy następującej składni:

corflags /32bit- aplikacja

11/12/2009

Ciekawe zgłoszenie błędu

Home

Jest to druga, poprawiona wersja tego postu. Za wcześniejsze pomyłki przepraszam.

W poście tym chciałbym opisać interesujący błąd. Wszystko zaczęło się od zgłoszenia od klienta dotyczącego problemów z wydrukami. Nie wchodząc w szczegóły okazało się, że cały problem sprowadza się do utworzenia dostatecznie dużej bitmapy. Co jednak ciekawe analiza pokazała, że system dysponuje znaczną ilością wolnej pamięci (ok 1.5 GB) podczas gdy do utworzenia bitmapy potrzebne było "raptem" kilkaset megabajtów. Tutaj dodam, że mówimy o systemie 32 bitowym.

Z pomocą przyszedł tutaj program vnmap, który służy do analizy pamięci wirtualnej i fizycznej procesu. Pokazał on, że proces rzeczywiście dysponuje znaczną ilością pamięci ale największy ciągły jej obszar to tylko 200 MB. Do zaalokowania bitmapy potrzeba natomiast właśnie ciągłego obszaru pamięci. Nie dotyczy to zresztą tylko bitmap, podobny problem możemy wystąpić przy ładowaniu bibliotek dll.

Taką sytuację nazywamy defragmentacją pamięci. Co było jej przyczyną? Zgodnie z tym co pokazał wspomniany program vnamp pamięć w dużym stopniu była poszatkowana przez biblioteki dynamiczne. Nie bez znaczenia jest tutaj fakt, że rozpatrywany przypadek dotyczył dość dużego systemu zbudowanego z wielu modułów.

Problem próbowałem zaleczyć przy użyciu narzędzia rebase.exe, które służy do ustawienia preferowanego adresu pod jaki ma zostać załadowana dll'ka. Testy niestety pokazały, że to nic nie daje.

Pytanie co jest przyczyną takiego położenia bibliotek w pamięci? Tutaj nie pozostaje mi nic innego jak rozłożyć ręce. Wcześniej byłem przekonany, że jest to związane z mechanizmem bezpieczeństwa, który losowo rozrzuca biblioteki po pamięci. Zwrócono mi jednak uwagę, że taki mechanizm (ASLR) pojawił się dopiero w Windows Vista. Sprawa jest więc otwarta. Jakieś pomysły?

Jak sobie z tym poradzić? Generalnie jednoznacznej odpowiedzi nie ma, ja znam trzy podejścia. Po pierwsze przejście na system 64 bitowy rozwiąże problem ale nie jest to zawsze możliwe. Po drugie można próbować wyeliminować konieczność alokacji tak dużej bitmapy ale może to być bardzo trudne. Można też użyć przełącznika /3GB, który pozwala procesom użytkownika użyć 3 GB pamięci wirtualnej zamiast domyślnych 2 GB ale nie jest to zalecane rozwiązanie.

Na zakończenie chciałbym podziękować koledze Damianowi z pracy, który analizował zgłoszenie klienta i podsunął mi pomysł na ten post.

30/11/2009

Uwierzytelnienie, ASP.NET i IIS

Home

Post ten postanowiłem napisać kiedy zorientowałem się, że kilku kolegów poświęciło sporo czasu aby rozwiązać problem z uwierzytelnieniem usługi sieciowej zainstalowanej na IIS'ie podczas gdy nie posiadali dokładnej wiedzy jak ten mechanizm działa. Problem udało się rozwiązać ale jestem przekonany, że można to było zrobić mniejszym nakładem czasu. Na początku zaznaczę również, że wpis ten dotyczy IIS w wersjach wcześniejszych niż IIS 7.

Jak wszyscy wiemy aplikacje ASP.NET i usługi sieciowe (ang. Web Services) hostowane są na serwerze IIS. Do swojego działania wymagają one dostępu do różnych zasobów. Aby możliwe było określenie czy aplikacja lub usługa posiadają uprawnienie do jakiegoś zasobu muszą one posiadać jakąś tożsamość czyli być uwierzytelnione (ang. authentication). Powstaje pytanie jaką tożsamość posiada aplikacja ASP.NET lub usługa sieciowa?

Na początek przyjrzyjmy się poniższemu scenariuszowi:
  • Użytkownik uruchamia przeglądarkę internetową i wpisuje adres aplikacji.
  • Serwer IIS otrzymuje żądanie i w zależności od ustawień żąda lub nie uwierzytelnienia.
  • Serwer IIS sprawdza czy użytkownik uwierzytelniony (bądź anonimowy) może uzyskać dostęp do żądanego zasobu.
  • Żądanie przekazywane jest do obsługi do silnika ASP.NET.
  • Aplikacja w zależności od ustawień ponownie żąda lub nie uwierzytelnienia się użytkownika.
  • Aplikacja obsługuje żądanie w razie potrzeby legitymując się pewną tożsamością.

Poziom IIS

Na poziomie IIS domyślnie włączone jest uwierzytelnienie anonimowe czyli innymi słowy brak uwierzytelnienia. W takim wypadku zgodnie z domyślnymi ustawieniami przyjmuje się, że użytkownik uwierzytelnił się jako IUSR_machinename. Dostępne są oczywiście inne tryby uwierzytelnienia np.: uwierzytelnienie zintegrowane z Windows (ang. Integrated Windows Authentication) czy uwierzytelnienia podstawowe (ang. Basic) gdzie hasło przekazywane jest czystym tekstem.

Uwierzytelnienia na poziomie IIS jest potrzebne aby stwierdzić czy użytkownik może żądać danego zasobu znajdującego się na serwerze. Zasobem tym może być aplikacja ASP.NET ale również inne rzeczy np.: obrazki, archiwa itd. Uwierzytelnienia na tym poziomie konfigurowne jest przy pomocy aplikacji administratora IIS: Control Panel -> Administrative Tools -> Internet Information Services.

Poziom silnika ASP.NET

Następnie mamy uwierzytelnienia na poziomie silnika ASP.NET. W tym przypadku można włączyć uwierzytelnienie anonimowe, zaimplementować cały mechanizm samemu czy użyć uwierzytelnienia Windows. Należy jednak zaznaczyć, że nie należy mylić uwierzytelnienia Windows na poziomie ASP.NET z uwierzytelnienia zintegrowanym z Windows na poziomie IIS.

Włączenie uwierzytelnienia Windows na poziomie ASP.NET oznacza, że silnik ASP.NET będzie widział użytkownika pod tożsamością wyznaczoną przez IIS. Innymi słowy IIS uwierzytelni użytkownika i przekaże jego tożsamość do ASP.NET. Metoda jaka zostanie użyta przez IIS do uwierzytelnienia zależy od ustawień. W szczególności, tak jak napisałem wyżej, może to być uwierzytelnienia zintegrowane z Windows ale również podstawowe czy inne. W przypadku braku uwierzytelnienia do silnika ASP.NET zostanie natomiast przekazana tożsamość anonimowa. Uwierzytelnienia na tym poziomie konfigurowane jest przy pomocy pliku web.config w sekcji <authentication>.

Tożsamość silnika ASP.NET

Teraz dochodzimy do setna sprawy czyli z jaką tożsamością działa silnik ASP.Net. Odpowiedź: „To zależy” :) Już odpowiadam ale zanim przejdziemy dalej należy jeszcze wyjaśnić czym jest impersonacja. Otóż impersonacja to mechanizm pozwalający przyjąć cudzą tożsamość i wykonywać w jej kontekście różne działania.

A więc jeśli wyłączona jest impersonacja (ustawienia domyślne) to silnik ASP.NET w przypadku systemów Windows Server 2000 i Windows XP działa na koncie o nazwie ASPNET, a przypadku Windows Server 2003 używa konta Network Service. Natomiast jeśli impersonacja jest włączona ASP.NET działa z tożsamością uwierzytelnionego użytkownika nawet jeśli jest to tożsamość anonimowa. Za te kwestie odpowiada sekcja <identity>.

Sprawa wygląda trochę inaczej w przypadku IIS 7, który posiada mocno zmienioną, a właściwie zupełnie inną architekturę w stosunku do IIS 6 i wcześniejszych. W szczególności w IIS 7 nie ma podziału na uwierzytelnienia na poziomie serwera i ASP.NET. Serwer IIS7 może być również konfigurowany z poziomu sekcji <system.WebServer> w pliku web.config. To jednak temat na kolejny wpis.

17/11/2009

SharpPcap

Home

WinPcap to dobrze znane wielu osobom narzędzie umożliwiające przechwytywanie i analizę ruchu sieciowego, jego generowanie oraz wiele innych rzeczy. W oparciu o WinPcap została napisana inna bardzo przydatna aplikacja WireShark. Narzędzi te są bezwątpienia bardzo przydatne ale co jeśli chcemy samemu zaimplementować algorytm analizujący ruch sieciowy. Dodatkowo najlepiej by było zrobić to w C# i nie babrać się w mechanizmach interoperacyjności z kodem niezarządzanym, bawić z atrybutem DllImport itd. Oczywiście chcemy również skorzystać z możliwości WinPcap.

Dokładnie w takiej sytuacji znalazłem się ostatnio. Wypróbowałem kilka rozwiązań ale szeroki uśmiech na mojej twarzy wywołała biblioteka SharpPcap. Została napisana w oparciu o WinPcap ale ukrywa przed użytkownikiem wszelkie odwołania do niezarządzanego kodu. Do tej pory przetestowałem tylko jej podstawowe funkcje ale wydaje się działać bardzo dobrze, a pracuje się z nią przyjemnie.

Zastanawiałem się nad zamieszczeniem tutaj jakiegoś przykładu użycia ale jestem zwolennikiem zasady "Po co robić coś co zostało już zrobione". Zainteresowanych zachęcam więc do pobrania biblioteki i zapoznania się z załączonymi przykładami, które są dobrze skomentowane. .

09/11/2009

Jeszcze o rysowaniu wykresów

Home

Kilka miesięcy temu pisałem o komponencie do rysowania wykresów firmy Xceed. Teraz postanowiłem przyjrzeć się dwóm innym komponentom: Komponent pierwszy jest do pobrania za darmo ze strony Microsoftu. Za drugi trzeba już płacić ale producent udostępnia wersję demonstracyjną.

Najpierw zainstalowałem komponent firmy Dundas. Od razu spodobał mi się program pokazujący przykłady różnych wykresów wraz z kodem. Dobre wrażenie sprawił również kreator zintegrowany z VS do konfigurowania kontrolki. Ogólnie pierwsze wrażenie bardzo pozytywne. Na dalszy ogień poszło rozwiązanie Microsoftu. W porównaniu do poprzednika od razu rzucił mi się w oczy brak kreatora i programu demonstracyjnego (można go pobrać oddzielnie tutaj).

Następnie zaczynam przyglądać się interfejsowi programistycznemu. Patrzę, patrzę i mam wrażenie deja vu. Pomimo pewnych różnic oba produkty mają bardzo, bardzo podobne API. Chwila zastanowienia i wpisuję w Google ''Microsoft Chart Control vs. Dundas Chart Control''. Pierwszy link i wszystko staje się jasne. Począwszy od roku 2007 Microsoft posiada prawa do własności intelektualnej firmy Dundas, a przejawem tego jest komponent Microsoft Chart Controls for Microsoft .NET Framework 3.5 oparty o kod Dundas Chart for .NET.

Podsumowując można za darmo pracować z bardzo fajną kontrolką pamiętając jednak o tym, że produkt Microsoftu posiada do pewnego stopnia ograniczoną względem oryginału funkcjonalności. Po drugie firma Dundas udziela wsparcie tylko na produkty zakupione bezpośrednio od nich. Nie jestem również pewny czy nowe usprawnienia, które wprowadzają programiści z Dundas są również dostępne w wersji Microsoftu (pewnie nie).

19/10/2009

Trochę o zwalnianiu zasobów

Home

Każdy dobry programista wie, że po skończeniu pracy z obiektem klasy implementującej interfejs IDisposable należy wywołać metodę Dispose (jawnie bądź nie jawnie). Dlatego kiedy ostatnio zobaczyłem kod, w którym programista beztrosko raz po raz tworzy ikonę, a następnie radośnie o niej zapomina powodując wzrost liczby obiektów GDI przez usta przeszły mi dość niecenzuralne słowa. Oczywiście od razu poprawiłem kod w mniej więcej taki sposób:
using(Icon icon = GetIcon())
{
   ...
}
Nic prostszego można powiedzieć. Jednak przy następnym uruchomieniu aplikacji ku mojemu zdziwieniu liczba obiektów GDI znowu zaczęła rosnąć. Zaglądam, więc do metody GetIcon. A tam widzę coś takiego:
return Icon.FromHandle(handle);
Nie kojarząc za bardzo metody FromHandle zaglądam do dokumentacji, a tam jest napisane coś takiego:

When using this method you must dispose of the resulting icon using the DestroyIcon method in the Win32 API to ensure the resources are released.

Kolejny mój krok to oczywiście sprawdzenie czy wywołanie DestroyIcon działa. Metodę tą należy zadeklarować w następujący sposób:
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet=CharSet.Auto)]
extern static bool DestroyIcon(IntPtr handle);
Jej użycie oczywiście rozwiązało problem. Co dociekliwsi mogą jeszcze zapytać czemu wywołanie Dispose nie wystarcza. Sprawa jest dość ciekawa. Okazuje się, że klasa Icon używa wewnętrznie DestroyIcon do zwalniania zasobów ale tylko i wyłącznie wtedy kiedy jest właścicielem tych zasobów to jest kiedy sama je zaalokuje. W momencie tworzenia ikony przy pomocy uchwytu dostarczonego z zewnątrz trzeba samemu zadbać o jego zwolnienie.

Reasumując w opisanym przypadku dwóch programistów zrobiło dwa poważne błędy. Pierwszy zapomniał o zwolnieniu zasobów, a drugi o dokładnym przeczytaniu dokumentacji. Metoda GetIcon powinna zostać napisana tak aby korzystający z niej programista nie musiał posiadać wiedzy "tajemnej" aby dobrze jej użyć.

11/10/2009

Kontrolki ASP.NET i zdarzenia

Home

Dzisiaj napiszę o rzeczy bardzo prostej ale, o której jednak zdarzyło mi się zapomnieć przez co zmarnowałem trochę czasu. Sytuacja miała miejsce kiedy pracowałem nad kontrolkę, która na swoim interfejsie publicznym między innymi udostępniała zdarzenia SelectionChanged. W kodzie wyglądało to jakoś tak:
...
public event EventHandler SelectionChanged;
...
Po jej napisaniu zabrałem się do testowania i jedną z rzeczy jaką chciałem sprawdzić było to czy zdarzenie jest generowane w odpowiednim momencie. Umieściłem więc kontrolkę na stronie w taki sposób:
...
<cc1:MyControl id="Control1" runat="server" SelectionChanged="Control1_SelectionChanged" />
...
Uruchamiam stronę i coś nie działa. Po krótkiej chwili dochodzę do wniosku, że coś musi być nie tak z zdarzeniem. Stawiam, więc w kodzie pułapkę i odświeżam stronę. Chwila debugu i konsternacja. Kod działa prawidłowo ale w momencie kiedy następuje próba wygenerowania zdarzenia okazuje się, że SelectionChanged równa się null.

Zaczynam sprawdzać czy nigdzie nie zrobiłem literówki itd. Uruchamiam kod ponownie i ciągle to samo. W końcu przypominam sobie, że aby deklaratywnie podczepić się pod zdarzenie kontrolki trzeba użyć przedrostka On. Kod powinien więc wyglądać jak poniżej:
...
<cc1:MyControl id="Control1" runat="server" OnSelectionChanged="Control1_SelectionChanged" />
...

05/10/2009

Raportowanie

Home
Ostatnio poznałem nieznany mi wcześniej, a prosty sposób tworzenia raportów i zapisywania ich do formatu PDF czy Excel. Działa on zarówno w kontekście aplikacji ASP.NET jak i w aplikacjach Windows Forms i innych. Mam tutaj na myśli klasy z przestrzeni nazw Microsoft.Reporting.WebForm (w przypadku aplikacji stacjonarnych chodzi o przestrzeń Microsoft.Reporting.WinForms).

W przestrzeni Microsoft.Reporting.WebForm znajdziemy wiele rzeczy, najważniejsze to po pierwsze kontrolka ReportViewer do prezentowania raportów. Po drugie klasy LocalReport oraz ServerReport służące odpowiednio wykonywaniu raportów lokalnie oraz zdalnie na serwerze. Źródłem danych dla raportów może być oczywiście relacyjna baza danych ale również obiekty biznesowe. Definicja raportu to dokument XML stworzony przy pomocy języka RDL (ang. Report Definition Language).

Zanim przeję dalej podam założenia/wymagania jakimi się kierowałem:
  • Raporty chcę zapisywać do formatu PDF i Excel.
  • Proces generowania raportu ma odbywać się lokalnie bez połączenia z bazą danych.
  • Dane mam zapisane w obiekcie klasy DataTable.
Po pierwsze musiałem znaleźć sposób dynamicznego generowania definicji raportów. Język RDL nie wygląda na trudny ale nie zmienia to faktu, że go nie znam. Po krótkich poszukiwaniach napotkałem na ten artykuł Lesson 4: Creating Code to Generate the Report Definition File. Zaprezentowany tam kod tworzy plik z definicją raportu jako dane wejściowe przyjmując: listę pól/kolumn, zapytanie do bazy danych oraz connection string. Jak widać kod jest przydatny ale nie do końca ponieważ nie odpowiada postawionym wymaganiom. Jak sie jednak okazało konieczne modyfikacje były bardzo proste. Pomijając zmianę argumentów wejściowych metody, delikatną modyfikację logiki wystarczyło, że jako zapytanie do bazy danych oraz connection string przekazałem pusty ciąg znaków String.Empty. Cała metoda jest dość długa ale w gruncie rzeczy nie zawiera niczego skomplikowanego. Dopowiem tylko, że liczba kolumn generowanego raportu zależy od liczby kolumn w przekezanym do metody obiekcie klasy DataTable. Pełny kod został pokazany poniżej.

Pokaż/Ukryj kod

Proces generowania raportu wynikowego w żądanym formacie również jest prosty. Poniższy fragment kody generuje raport w formacie PDF. Żeby wytworzyć arkusz programu Excel to metody Render należy przekazać ciąg znaków "Excel". Zmienna reportPath powinna zawierać ścieżkę do pliku z definicją raportu stworzonego przez pokazaną wcześniej metodę. Zmienna dataTable to z kolei tabela z danymi do zapisania w żądanym formacie. Powinna to być ta sama tabela, która została przekazana do metody GenerateRdl.
...
FileStream fs = File.OpenWrite( reportPath);
GenerateRdl(fs, dataTable);

LocalReport lr = new LocalReport();
lr.ReportPath = reportPath;
lr.DataSources.Add(new ReportDataSource("DummyDataSet", dataTable));

string deviceInfo = "<DeviceInfo><SimplePageHeaders>True</SimplePageHeaders></DeviceInfo>";
string mimeType = null;
string encoding = null;
string ext = null;
string[] streamids = null;
Warning[] warnings = null;
byte[] bytes = lr.Render("PDF", deviceInfo, out mimeType, out encoding, out ext, out streamids, out warnings);
...