SharePoint - Dispose  Udostępnij na: Facebook

Autor: Jakub Gutkowski

Opublikowano: 2011-08-01

SharePoint udostępnia dwa modele pracy – z aplikacji klienta lub bezpośrednio z aplikacji na serwerze, czy będzie to wolno stojąca, czy też WebPart na stronie. Mimo podobieństw w nazwach obiektów różnica pomiędzy tym, jak kod działa, jest ogromna.

Kod z wykorzystaniem Client Object Model (Client OM, nie mylić z COM) nie powoduje żadnych skutków ubocznych po stronie serwera, chyba że uda nam się znaleźć błąd. Całość działa na komputerze klienta i wykorzystuje zasoby dostępne na danej maszynie, nie wykorzystując zasobów serwerowych przynajmniej bezpośrednio (pośrednio, poprzez wykonanie zapytania tak – w końcu SharePoint musi pobrać informacje z bazy danych i zwrócić je nam). Nie musimy więc koncentrować się na problemach, jakie mogą wystąpić po stronie serwera – kod na serwerze bezpośrednio wpływa na wydajność całego środowiska, zużywa on zasoby lokalne, które mogą być niezbędne do poprawnego i bezbłędnego działania całego systemu operacyjnego. Pisanie kodu bez jego uprzedniego przemyślenia może bardzo łatwo doprowadzić zarówno do zużycia czasu procesora, jak i pamięci RAM, co pośrednio może spowodować częste odświeżanie application pool, prowadzące do bardzo wolnych odpowiedzi na żądania klienta o wyświetlenie strony. Sytuację można podnieść do ekstremum i powiedzieć, że jeżeli nie zwracamy uwagi na to, co robimy i jak robimy, możemy pozbawić serwer stabilności – a nie ma nic gorszego niż pokazywanie yellow screenof deathużytkownikowi końcowemu.

Na naszą korzyść, w porównaniu do SharePoint 2007, działa postanowienie Microsoft o udostępnianiu produktów serwerowych jedynie w wersjach 64-bitowych. Dzięki czemu znane wcześniej ograniczenia alokacji pamięci przestają istnieć – zamiast 800MB RAM na proces application poolw IIS w3wp.exe mamy odpowiednio więcej RAM, co oczywiście pośrednio powoduje, że dużo później dostaniemy informację zwrotną o tym, że jednak problem z zarządzaniem pamięcią istnieje.

Zarządzanie pamięcią. To, jak .NET zarządza pamięcią, można w bardzo prostych słowach przeczytać tutaj: http://bit.ly/ghV6KS, zaś informację o tym, dlaczego przy 800 MB w SharePoint 2007 następował problem – tutaj: http://bit.ly/eIbX5k. Polecam zapoznaniem się z obydwoma artykułami.

Problem z zasobami w trakcie pracy z SharePoint spowodowany jest przez biblioteki SharePointa, które wewnętrznie (niewidocznie dla programisty) odwołują się do komponentów COM. Te komponenty nie są zarządzanie przez garbage collector w .NET – tzn. po zamknięciu naszej aplikacji zaalokowane zasoby przez wykorzystywane komponenty COM nie są zwalniane dopóty, dopóki bezpośrednio proces z nimi związany nie zostanie zatrzymany. Wyobraźcie sobie sytuacje, że co 10 minut musicie zrestartować komputer produkcyjny, bo jest przeciek pamięci i nie jesteście w stanie nawet otworzyć notatnika – raczej nikt z tego powodu nie będzie zadowolony.

Garbage Collector.  Wspomniałem wcześniej, że nasz kod może zużywać czas procesora. Jest to głównie spowodowane przez GC, który próbuje zwolnić pamięć zaalokowaną przez nasz kod, uruchamiając się odpowiednio często, co powoduje zarówno zużycie czasu procesora, jak i blokowanie naszego kodu do czasu ukończenia przejścia zwalniania zasobów przez GC.

Jednym z takich obiektów, które bezpośrednio powiązane są z komponentem COM, jest SPRequest odpowiedzialny za komunikację kod <-> baza danych. Za każdym razem, kiedy poprosimy o dane, na przykład w postaci tytułu witryny, SharePoint wewnętrznie tworzy obiekt SPRequest (jeżeli nie został on utworzony wcześniej), który jest w stanie nam zwrócić wymaganą informację. Jednak aby to móc zrobić, potrzebuje odwołać się do komponentu COM, który już przez GC nie jest zarządzany. Więcej szczegółów o SPRequest oraz o sposobie jego działania można przeczytać tutaj.

Dlatego, ze względów wyżej wymienionych, podczas pracy z modelem obiektowym SharePoint musimy zwracać szczególną uwagę na zwalnianie pamięci, która została zaalokowana pośrednio lub bezpośrednio przez nasz kod.

Biblioteka SharePoint wspiera nas w tym, jednak wymaga ona od nas bezpośredniej prośby o to. Prośba ta objawia się poprzez wykonanie metody Dispose na obiekcie implementującym interfejs IDisposable. Klas w bibliotece SharePoint (Microsoft.SharePoint.dll), które bezpośrednio implementują taki interfejs, jest 1161, jednak my najczęściej spotykać się będziemy z dwoma klasami  SPSite i SPWeb.

Dispose i null. Przypisanie wartości null do obiektu nie powoduje zwolnienia zasobów przez obiekt zaalokowany. Dispose i null są to dwie zupełnie różne rzeczy i bardzo dobry opis, na czym polega różnica oraz kiedy tak naprawdę warto użyć przypisania null, można znaleźć tutaj: http://bit.ly/efsvWj.

Oznacza to, że kiedy tworzymy obiekt SPSite, musimy pamiętać, aby poprosić go o zwolnienie zasobów, czyli kod poniżej (zwracający nazwę portalu) jest niepoprawny i powoduje niezwolnienie zaalokowanych zasobów przez obiekt SPSite:

static void Sample1()
{
    SPSite site = new SPSite("http://gutek-dev");
    Console.WriteLine(site.PortalName);
}

Można kod naprawić poprzez wywołanie metody Dispose na końcu:

static void Sample2()
{
    SPSite site = new SPSite("http://gutek-dev");
    Console.WriteLine(site.PortalName);
    site.Dispose();
}

Na szczęście, nie zawsze musimy bezpośrednio sami wołać Dispose i możemy polegać na wyrażeniu usinghttp://bit.ly/fQCYCP. Dzięki czemu nasz kod możemy zamienić na:

static void Sample3()
{
    using(SPSite site = new SPSite("http://gutek-dev"))
    {
        Console.WriteLine(site.PortalName);
    }
}

Metody Close. W .NET istnieje kilka typów implementujących metodę Close, przez to pojawia się czasami pytanie, czy powinniśmy wywołać metodę Close, czy dostępną metodę Dispose? W przypadku SharePoint jest podobnie – SPSite i SPWeb implementują metodę Close. To, czy wykonamy polecenie Dispose, czy Close, akurat tutaj nie ma znaczenia (Dispose wywołuje metodę Close), jednak różnica polega na tym, iż bez Dispose nie moglibyśmy wykorzystać using i polegać na kompilatorze od C#, który zamieni nam kod na odpowiednią postać. Jeśli jednak korzystamy z modelu obiektowego bez wykorzystania using, to możecie znaleźć słowo Close bardziej dokładniej tłumaczące wasze zamiary. Jednak jest to kwestia przyzwyczajenia – pracując w zespole, lepiej ustalić wspólnie jaką metodę się uruchamia, aby potem nie nastąpił problem ze zrozumieniem kodu.

Popatrzmy na jeszcze jeden przykład Dispose, tym razem na obiekcie SPWeb. Nasza aplikacja ma za zadanie wypisać nazwę portalu i witryny:

static void Sample3()
{
    SPSite site = new SPSite("http://gutek-dev");
    SPWeb web = site.OpenWeb();

    Console.WriteLine("{0} - {1}", site.PortalName, web.Title);
}

W tym przypadku mamy dwa problemy – zarówno SPSite, jak i SPWeb spowodują wyciek. Wiedząc, że możemy wykorzystać using na SPSite, możemy nasz kod napisać tak:

static void Sample4()
{
    using(SPSite site = new SPSite("http://gutek-dev"))
    {
        SPWeb web = site.OpenWeb();

        Console.WriteLine("{0} - {1}", site.PortalName, web.Title);
    }
}

Teraz pozostał nam wyciek z SPWeb, ale czy aby na pewno? W SharePoint 2007 odpowiedź brzmiała tak, na pewno, zaś w SharePoint 2010 brzmi ona „nie”. SPSite podczas zwalniania zasobów zwolni też zasoby zaalokowane przez wszystkie obiekty SPWeb, które zostały za pomocą SPSite otwarte. Możemy się o tym przekonać, wykonując następujący kod:

static void Sample5()
{
    SPSite site = new SPSite("http://gutek-dev");
    SPWeb web = site.OpenWeb();

    Type t = web.GetType();
    FieldInfo isClosedField = t.GetField("m_closed", 
        BindingFlags.Instance | BindingFlags.NonPublic);
    FieldInfo requestField = t.GetField("m_Request", 
        BindingFlags.Instance | BindingFlags.NonPublic);

    Console.WriteLine("{0} - {1}", site.PortalName, web.Title);

    bool isWebClosedBeforeDispose = (bool)isClosedField.GetValue(web);
    object requestBeforeDispose = requestField.GetValue(web);

    Console.WriteLine("Web is closed: {0} and SPRequest is null: {1}", 
        isWebClosedBeforeDispose, 
        requestBeforeDispose == null);

    site.Dispose();
    Console.WriteLine("SPSite disposed");

    bool isWebClosedAfterDispose = (bool)isClosedField.GetValue(web);
    object requestAfterDispose = requestField.GetValue(web);

    Console.WriteLine("Web is closed: {0} and SPRequest is null: {1}", 
        isWebClosedAfterDispose, 
        requestAfterDispose == null);
}

Kod. Powyższy kod może być trochę skomplikowany i raczej nie powinniśmy się nim przejmować. Został on jednak podany, by zademonstrować, że obiekt SPWeb jest zwalniany z pamięci. To, co powyższy kod robi, to wykorzystuje refleksję dostępną w .NET i odwołuje się do wartości prywatnych zmiennych w klasie SPWeb.

Powinniśmy uzyskać następujący wynik:

Portal Name - Team Site
Web is closed: False and SPRequest is null: False
SPSite disposed
Web is closed: True and SPRequest is null: True

Jednak trzeba pamiętać o tym, by potem z obiektu SPWeb już nie korzystać. SPSite wywołuje metodę Close na SPWeb, która ustawia zmienną m_closed na true. Jeżeli ta zmienna jest true, to kolejne odwołanie do Close (Dispose wywołuje Close) nie spowoduje usunięcia obiektu SPRequest – który zostanie na nowo utworzony, jeżeli zapytamy na przykład o liczbę list na witrynie (nie zostanie on utworzony przy ponownym odpytaniu o tytuł witryny, gdyż ta wartość po pierwszym odpytaniu jest przypisana do zmiennej prywatnej w klasie SPWeb), powodując niechciany wyciek.

Pomimo że SPSite zarządza za nas obiektami SPWeb, to czasami jednak warto pozbyć się bezpośrednio takiego obiektu, Dla przykładu, w pętli tworzymy 20 SPWeb, które powodują, iż zasoby są 20-krotnie alokowane. Taka operacja dla jednego użytkownika naraz może być przydatna, ale pomyślmy, że w tym samym momencie 100 użytkowników wykonuje taką samą funkcję. Nagle istnieje 2000 obiektów SPWeb, każdy zabiera zasoby zarówno zarządzane, jak i niezarządzane. Dlatego dobrą praktyką jest zamienienie powyższego kodu na:

static void Sample6()
{
    using(SPSite site = new SPSite("http://gutek-dev"))
    using(SPWeb web = site.OpenWeb())
    {
        Console.WriteLine("{0} - {1}", site.PortalName, web.Title);
    }
}

Mamy teraz pewność, że zasoby zaalokowane przez nasz obiekt przestaną istnieć.

Wiedząc o tym, że trzeba wywoływać Dispose na tych dwóch obiektach, nasza praca byłaby dość prosta – za każdym razem, kiedy tworzymy taki obiekt, wykonujemy Dispose i tyle. Jednak SharePoint jest sprytny – na tyle sprytny, że czasami tworzy za nas takie obiekty w miejscach, w których w ogóle byśmy się nie spodziewali i nawet o nich nie wiemy. Taki mały sabotaż ze strony twórców SharePointa. Na szczęście Microsoft poświęcił dużo czasu, aby znaleźć dla nas takie miejsca i wypisać je w artykułach: http://bit.ly/dVfSst i http://bit.ly/ges9ZL – polecam dokładnie zapoznać się z nimi i ich wydrukowaną wersję mieć na biurku w trakcie pracy, by zawsze można było do niej zajrzeć dopóki nie zapamięta się wszystkich takich miejsc (co do prostych rzeczy nie należy). Artykuły prezentują przykłady poprawnego i złego wykorzystania obiektów SharePoint, wskazując miejsca, gdzie może nastąpić wyciek pamięci i opisują dlaczego. Warto też od czasu do czasu zaglądać na ich stronę i sprawdzić, czy nie ma aktualizacji artykułu – kto wie, może coś nowego się w tej kwestii pojawiło albo nowy obiekt zostanie opisany.

Niezależnie od wiedzy, gdzie i jak stosować Dispose, warto też znać miejsca, gdzie nie powinno się stosować Dispose, gdyż inny kod jest za to odpowiedzialny. Miejsca takie są opisane pod koniec postu na tym blogu: http://bit.ly/eRXZRj

Pomoc ze strony MS

Microsoft udostępnia narzędzie o nazwie SharePoint Dispose Checker Tool, którego celem jest analiza naszego kodu pod kątem wystąpienia potencjalnych przecieków pamięci oraz miejsc, w których nie powinniśmy wykonywać metody Dispose. Narzędzie to integruje się z Visual Studio i jest do pobrania stąd: http://bit.ly/gjxqtt

Po instalacji powinniśmy w Visual Studio w menu Tools na samej górze móc wybrać narzędzie SharePoint Dispose Check:

 

Jego konfiguracja jest bardzo prosta, wystarczy zaznaczyć kiedy ma się uruchamiać i dla jakich projektów z aktualnego solution powinien przeprowadzać analizę. Dla naszego przykładu – z pobraniem nazwy witryny i nazwy portalu:

static void Sample3()
{
    SPSite site = new SPSite("http://gutek-dev");
    SPWeb web = site.OpenWeb();

    Console.WriteLine("{0} - {1}", site.PortalName, web.Title);
}

Konfiguracja narzędzia może (w zależności od nazw projektów i solution oraz opcji, jakie się wybierze) wyglądać tak:

Przy buildzie (uwaga, naruszenie reguł Dispose dla SharePoint nie blokuje buildu, nawet gdy pojawią się błędy/złamania reguł, nie działa on tak samo jak FxCop) natomiast powinniśmy dostać taką oto listę błędów:

Plusem jest to, że wraz z błędem dostajemy link do jego szczególnego opisu i z przykładem, jak powinniśmy rozwiązać ten problem.

Narzędzie jest bardzo przydatne i powinno stanowić nieodłączną część naszego procesu wytwarzania oprogramowania na SharePoint – bardzo polecam jego używanie, choć też nie zawsze ma ono rację, czasami możemy chcieć tak postąpić (nie robić Dispose na obiekcie). Możemy więc taki kod oznaczyć odpowiednim atrybutem, co spowoduje, że narzędzie pominie jego sprawdzenie. Więcej na ten temat można przeczytać pod koniec blog postu tutaj: http://bit.ly/aTafKg.

Podsumowanie

Dowiedzieliśmy się z tego artykułu, co może się stać, kiedy nie przemyślimy naszego kodu, jakie to niesie konsekwencje w wydajności serwera oraz dlaczego tak się dzieje. Nauczyliśmy się, że aby dobrze pisać aplikację pod SharePoint, musimy zważać na zasoby, jakie są przez SPS alokowane oraz jak za pomocą narzędzia SharePoint Dispose Checker możemy wspomóc nasz proces wytwarzania oprogramowania. Poznaliśmy także kilka przypadków, kiedy i jak wykonać Dispose i dlaczego using jest lepszym rozwiązaniem od bezpośredniego wywoływania Dispose.

To jednak, jak i kiedy powinniśmy zwalniać zaalokowane zasoby, powinno zależeć od naszego kodu. Jeżeli potrzebujemy przechowywać obiekt SPWeb lub SPSite w klasie jako zmienną lokalną, to wykorzystanie using raczej się nie sprawdzi. W takich wypadkach warto zawsze się zastanowić, kiedy powinniśmy wywołać metodę Dispose lub Close i czy to nasza klasa powinna być za to odpowiedzialna, czy kod wykorzystujący naszą klasę. To są jednak decyzje projektowe – jeżeli wasz projektant nie wie o tym, raczej nie zaprojektuje aplikacji tak, aby uwzględnić Dispose, dlatego bardzo ważne jest, by wziąć to pod uwagę na początku, a nie na końcu pisania kodu. Niektóre zmiany bardzo ciężko jest wprowadzić na kilka dni przed oddaniem aplikacji – wprowadzenie Dispose należy do takich.