lambdaniel

Daniel Janus o języku, typografii, książkach, programowaniu w Clojure i nie tylko

Posted in Fablo

Pierwsze użycie: protokoły i git-bisect

Lubię rozwiązywać problemy przy użyciu narzędzi, których dotychczas nie znałem albo znałem tylko teoretycznie, na zasadzie “wiem, że istnieje coś takiego i do czego z grubsza służy”. Jeszcze przyjemniej jest, kiedy uzyskane rozwiązanie okazuje się czytelniejsze, zrozumialsze, szybsze albo pod innymi względami lepsze niż wersja używająca tylko dotychczasowego “arsenału”. W tych dniach zdarzyło mi się tego doświadczyć dwukrotnie, gdy pracowałem nad Fablo.

Fablo to napisany w Clojure silnik wyszukiwarki dla polskich sklepów internetowych: zna odmianę polskich wyrazów, obsługuje literówki i błędy ortograficzne. Radzi sobie też z wyrazami wpisanymi bez polskich znaków, a więc wie, że “papryka zolta” to to samo, co “papryka żółta”. Działa to na ogół zupełnie dobrze, ale wciąż ulepszamy wyszukiwarkę i eliminujemy błędy polegające na tym, że na jakieś zapytanie nie wyszukują się niektóre produkty, choć powinny. Tym razem błąd brzmiał: zapytanie “sledzie” zwraca inne wyniki niż “śledzie”.

Analiza problemu ujawniła, że algorytm odpowiedzialny za “polszczenie” zapytania powinien być stosowany szerzej. Mamy dwie implementacje pewnej struktury danych używanej w tym algorytmie: jedna z nich jest czysto funkcyjna i zbudowana z rdzennie clojurowego tworzywa — map i wektorów; druga zaś jest ukryta za fasadą używanej przez nas biblioteki Javowej o prostym interfejsie, a więc explicite do tej pory z niej nie korzystaliśmy. Algorytm polszczenia był uruchamiany tylko na pierwszej implementacji (co wystarczy w wielu przypadkach, ale nie zawsze), a powinien być na obu.

Koncepcyjnie te dwie implementacje są podobne, ale mają zupełnie różne API — zrazu wydawało mi się więc, że będę musiał pisać drugą wersję funkcji realizującej “polszczenie”. To jednak nie byłoby optymalne: duplikacja kodu oznacza dwa razy więcej okazji do popełnienia błędu i konieczność pamiętania o wprowadzeniu zmian każdorazowo w obu miejscach. Z pomocą przyszły protokoły — nowy element języka wprowadzony w Clojure 1.2.

Protokół jest czymś bardzo zbliżonym do znanego z Javy interfejsu: to zestaw deklaracji nazwanych funkcji i ich argumentów, jednak bez implementacji. Implementacje definiuje się dla konkretnych typów. Mogą to być zarówno typy definiowane w Clojure (przy użyciu konstrukcji deftype), jak i istniejące klasy Javowe. Można więc zadeklarować w protokole jakąś operację (nazwijmy ją blabalizacją), po czym zdefiniować, co oznacza blabalizowanie liczb, a co napisów. Efekt jest taki, jak gdybyśmy w Javie jakoś zmusili klasy java.lang.Integer i java.lang.String do implementowania interfejsu Blabalize; możemy wołać odpowiednią funkcję bezpośrednio na obiektach odpowiednich klas, bez żadnych wrapperów!

Zacząłem więc od zdefiniowania protokołu, opisującego dwie operacje, jakich można dokonywać na mojej strukturze danych. Potem przerobiłem implementację algorytmu “polszczącego” zapytania tak, aby korzystała tylko z tych dwóch operacji, co uniezależniło ją od implementacji “pod spodem”. Napisanie Clojurowej implementacji protokołu było proste; trudniejsze okazało się odpowiednie skorzystanie z biblioteki Javowej, ale i to udało się zrobić w miarę szybko.

Efekt: brak duplikacji kodu i tylko nieznacznie zmodyfikowana dotychczasowa wersja. A przy tym udało się nagiąć bibliotekę Javową do robienia rzeczy, do których nie była zaprojektowana, bez ingerencji w jej kod. Clojure nie przestaje mnie zadziwiać.

Innym razem zauważyłem, że od kilku commitów przy próbie uruchomienia testów jednostkowych i regresji w logach testowania pojawia się komunikat, którego zdecydowanie nie powinno tam być. Mógłbym zabrać się do tego testując okolice pojawienia się komunikatu w REPL, ale tym razem postanowiłem zrobić to inaczej. Ponieważ używamy Git do kontroli wersji, wykorzystałem narzędzie git-bisect, aby znaleźć commit wprowadzający komunikat. Potrzebuje ono do działania trzech rzeczy: identyfikatorów commitu “dobrego” i “złego” oraz skryptu, który odpowiada na pytanie, czy dany commit jest “dobry” czy “zły” (zwracającego kod wyjścia odpowiednio 0 albo 1). Skrypt wyglądał mniej więcej następująco:

#!/bin/bash
cake clean
cake deps
cake proto
sleep 10
cake release
rm *.log
make test
if grep -q "PODEJRZANY_KOMUNIKAT" fablo*log; then
  exit 1
else
  exit 0
fi

(Cake jest narzędziem, którego używamy do budowania oprogramowania; 10-sekundowa przerwa między cake proto a cake release jest remedium na błąd w cake.)

Potem wystarczyło tylko:

$ git bisect start HEAD dobry_commit --
$ git bisect run check.sh

i już wiedziałem, w którym miejscu się zepsuło, a stąd było już niedaleko do znalezienia przyczyny problemu.

Posted on 2011-01-13

Filmujmy błędy!

Każdy programista (o ile jego oprogramowanie jest używane przez kogoś jeszcze poza nim samym) dostał kiedyś zgłoszenie błędu w programie. Wielu pracuje na co dzień z systemami śledzenia błędów, wielu zgłasza błędy w programach, których sami używają.

Dobre zgłoszenie powinno pomóc programiście odtworzyć błąd. Instrukcja odtworzenia jest bodaj najważniejszym z trzech nieodzownych elementów zgłoszenia. (Pozostałe dwa, jak pisze Joel Spolsky, to odpowiedzi na pytania: co powinno było się stać oraz co naprawdę się stało.) Na ogół w systemach śledzenia błędów opisujemy po prostu prozą kroki, które wykonaliśmy, a które doprowadziły do błędnej sytuacji.

Dokładne opisanie wykonanych czynności jest jednak trudne. Znaczenie może mieć każdy, nawet najmniejszy szczegół, łatwo więc o nieścisłość. Zamiast “otworzyłem plik” lepiej napisać “nacisnąłem Alt-O” albo “kliknąłem w menu Plik i kliknąłem polecenie Otwórz”, w zależności od tego, co naprawdę zrobiliśmy. W świetnym eseju Jak efektywnie zgłaszać błędy, z którego zaczerpnąłem ten przykład, Simon Tatham podaje inną, efektywniejszą metodę zgłaszania: należy posadzić programistę przed swoim komputerem i pokazać mu, w jaki sposób doprowadzamy do błędu.

Ta metoda jest dobra, kiedy da się zastosować. Problem pojawia się w przypadku, kiedy programista jest fizycznie na drugim końcu świata. Nie może wtedy zobaczyć monitora użytkownika i patrzeć mu na ręce… albo przynajmniej do niedawna nie mógł.

Od jakiegoś czasu w Fablo dołączamy do zgłoszeń błędów krótkie filmiki — screencasty z komentarzem głosowym. Z każdym kolejnym błędem rozwiązanym dzięki wskazaniu w taki sposób coraz bardziej przekonuję się do rozlicznych zalet tej metody. Przede wszystkim film jest dokładnym zapisem sesji. Widać wszystko, co się działo krok po kroku, i nie trzeba prosić o dodatkowe szczegóły, co zabiera czas i naprawiającego, i zgłaszającego. Film można odtwarzać wielokrotnie i dowolnie po nim nawigować, a nawet spowalniać; to nawet lepsze niż patrzenie na żywo na pracę użytkownika z programem. Jeśli mamy do czynienia z heisenbugiem (“wywala się mniej więcej co pięć minut i w różnych sytuacjach, za cholerę nie mam pomysłu!”), możemy po prostu włączyć nagrywanie i zapomnieć o nim, pracując normalnie do momentu wystąpienia błędu. Film można wtedy obciąć do ostatnich dwóch-trzech minut i wysłać.

Co więcej, sporządzenie filmiku bardzo mało kosztuje. Na obie używane przez nas platformy dostępne są narzędzia pozwalające łatwo nagrać film. Dzięki Snapz Pro X na Mac OS X nagranie jest tak proste jak naciśnięcie Shift-Cmd-3 na początku i na końcu filmu; Record My Desktop na Linuksa i FreeBSD jest nieco trudniejszy w obsłudze, ale wciąż łatwy.

Wypadałoby wspomnieć o minusach filmów. Przychodzą mi do głowy dwa. Jednym jest fakt, że nie każdy błąd można zgłosić w ten sposób, a mówiąc ściślej: błąd w nie każdym oprogramowaniu. Gdy pracuje się z bibliotekami zamiast z okienkowym lub przeglądarkowym oprogramowaniem, wyłapywane błędy są innej natury i polegają na ogół na nieoczekiwanym zachowaniu konkretnej funkcji lub klasy. To, nolens volens, trzeba opisać.

Druga sprawa to ilość miejsca zajmowanego przez filmiki i czasu potrzebnego na ich wrzucenie do systemu kontroli błędów. To zapewne jeszcze do niedawna był powód, dla którego rzadko widzi się takie zgłoszenia np. w oprogramowaniu open source. Jednak problem ten staje się mało znaczący wraz z upowszechnieniem się szybkich łącz. Typowa wielkość naszego filmiku to ok. 1.5-2 MB; to naprawdę nie jest dużo, a FogBugz pozwala na bardzo łatwe — jednym przeciągnięciem myszy — dodanie screencastu do raportu błędu. Większe rzeczy z łatwością można udostępnić w Dropbox.

Filmujmy więc błędy!

Posted on 2010-11-19