Pokazywanie postów oznaczonych etykietą IDEA. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą IDEA. Pokaż wszystkie posty

piątek, 17 sierpnia 2012

Cuda na Javie część 2

W poprzednim wpisie CUDA na Javie opisałem trochę walki z technologią i ze sprzętem, dotyczącym programowania CUDA za pomocą Javy. Bibliotekę jCUDA odrzuciłem ze względu na brak ogólnodostępnej dokumentacji i możliwości rejestracji się, aby się dobrać do tej dokumentacji. Skorzystałem więc z konkurencyjnego rozwiązania: JCuda w wersji 0.3.2a dla CUDA 3.2 (jako że z nowszą wersją nie mogłem się dogadać). Czeka nas jeszcze doinstalowanie czegoś i konfiguracja, ale na początek trochę teorii.

Chcąc pisać w CUDA'ch, tworzymy pliki tekstowe z rozszerzeniem *.cu. Te następnie są on kompilowane z pomocą nvcc (kompilatora dostarczanego przez Nvidię w CUDA Toolkit) do jednego z kilku formatów: (np. PTX, CUBIN, EXE). Pierwszy z nich zawiera instrukcje asemblerowe, które są czytelne dla ludzi (jak ktoś oczywiście lubi / umie). Jest to również uniwersalny format, gdyż przed wykonaniem takiego kodu, jest on kompilowany pod konkretny układ GPU. Natomiast format CUBIN, to już format binarny pod konkretny procesor.

Więcej szczegółów na temat procesu kompilowania i linkowania Cudy zamieszczam na poniższym rysunku (schemat ze strony: http://www.think-techie.com/2009/09/gpu-computing-using-jcuda.html):


W plikach *.cu możemy umieszczać typowy kod C jak i tzw. The Kernel Code. Jest to kod, który wykonuje się na urządzeniu (ang. device) obsługującym CUDA. Kompilator nvcc nie jest przez to samowystarczalny. Do jego poprawnego działania wymagany jest jeszcze kompilator C, np. Visual Studio C++. Początkowo zainstalowałem VC++ 2010 Express Edition, ale się okazało, że moja wersja CUDA współpracuje z MCVC 8.0 lub 9.0. Zainstalowałem więc Visual Studio 2008 i przy próbie kompilacji przykładu Creating kernels dostałem następujący błąd:

nvcc fatal   : Cannot find compiler 'cl.exe' in PATH

Ok, szukamy cl.exe I dodajemy do PATH, czyli ***\Microsoft Visual Studio 9.0\VC\bin.

Teraz robi się ciekawie. Uruchamiając przykład, dostaję:
nvcc -m64 -ptx JCudaVectorAddKernel.cu -o JCudaVectorAddKernel.ptx
nvcc process exitValue -1
errorMessage:
nvcc fatal   : Cannot find compiler 'cl.exe' in PATH

ale uruchamiając to samo „z palca” dostaję:

nvcc fatal: Visual Studio configuration file '(null)' could not be found for installation at...

Musiałem doinstalować X64 Compilers and Tools



I zaczęło śmigać.

Czasem może być jeszcze wymagane dodanie do PATH’a: ***\Microsoft Visual Studio 9.0\VC\bin\amd64

Można również się natknąć na poniższy problem:

host_config.h(96) : fatal error C1083: Cannot open include file: 'crtdefs.h': No such file or directory

Aby go rozwiązać, trzeba jeszcze wskazać CUDA, gdzie są pliki nagłówkowe z kompilatora C. Aby to zrobić, należy w pliku nvcc.profile (w katalogu bin instalacji CUDA) w sekcji INCLUDES dodać ścieżkę do odpowiedniego katalogu:

INCLUDES        +=  "-I$(TOP)/include" "-I$(TOP)/include/cudart" "-Id:/Microsoft Visual Studio 9.0/VC/include" $(_SPACE_)

I już działa. Rozwiązanie ostatniego problemu znalazłem tutaj: http://blogs.hoopoe-cloud.com/index.php/2009/09/cudanet-examples-issues

Skoro już wszystko działa (mam nadzieję że niczego nie ominąłem) czas na jakiś kod. Sztandarowym przykładem pokazującym potęgę GPU jest sumowanie wektorów. Poniżej przedstawię przykład ze strony JCuda Code samples. Musiałem jednak trochę go zmodyfikować, gdyż tamtejszy przykład jest przygotowany dla cudów 4.0 i korzysta z metod, których w mojej wersji API (0.3.2a) nie ma. Podrasowałem również kod samego kernela, aby wyjaśnić pewne zjawiska tam zachodzące. Kod biblioteki jest na licencji MIT/X11 License więc mogę modyfikować, z zachowaniem informacji o autorze.

Zacznijmy od kodu Javowego:

// Enable exceptions and omit all subsequent error checks
JCudaDriver.setExceptionsEnabled(true);

// Create the PTX file by calling the NVCC
String ptxFileName = preparePtxFile("JCudaVectorAddKernel.cu");

// Initialize the driver and create a context for the first device.
cuInit(0);
CUdevice device = new CUdevice();
cuDeviceGet(device, 0);
CUcontext context = new CUcontext();
cuCtxCreate(context, 0, device);

// Load the ptx file.
CUmodule module = new CUmodule();
cuModuleLoad(module, ptxFileName);

// Obtain a function pointer to the "add" function.
CUfunction function = new CUfunction();
cuModuleGetFunction(function, module, "add");

Mamy tutaj trochę rzeczy związanych z ustawieniem sterownika Cuda. Początkowo jest włączane rzucanie wyjątków z metod biblioteki, gdy coś pójdzie nie tak. Samo już istnienie takiej metody świadczy według mnie o autorze, któremu jest bliżej do C niż do Javy. Moje obawy zostały potwierdzone przez początkowe formatowanie kodu w źródłowym pliku.

Następnie w linii 5tej ładujemy nasz plik *.cu, a dokładniej, jeżeli nie istnieje dla niego odpowiednik w formacie PTX, to nasz plik źródłowy z kodem kelnera jest kompilowany, w trakcie działania naszej aplikacji. Jest to metoda napisana przez autora przykładu i jak ktoś chce się jej przyjrzeć bliżej to niech zajrzy do kodu.

Wracając do opisanego przykładu dalej następuje inicjalizacja naszego urządzenia i utworzenie dla niego kontekstu. Dalej w linii 15stej nastepuje utworzenie modułu i załadowanie do niego skompilowanego pliku PTX, zwróconego przez metodę preparePtxFile(). W ostatniej linijce w końcu dobieramy się do naszej funkcji, którą napisaliśmy w naszym pliku *.cu.

Dobra teraz czas na przygotowanie danych wejściowych do naszej funkcji:

int numElements = 100000;

// Allocate and fill the host input data
float hostInputA[] = new float[numElements];
float hostInputB[] = new float[numElements];
for (int i = 0; i < numElements; i++) {
    hostInputA[i] = (float) i;
    hostInputB[i] = (float) i;
}

Tworzymy dwa wektory o długości 100k i wypełniamy kolejnymi liczbami. Zabieg ten ma na celu łatwą weryfikację poprawności uzyskanego wyniku. Stworzone dane wejściowe zostaną utworzone na stercie i nie będą dostępne dla GPU. Po za tym odczyt ten mógłby być bardzo długi (z powodu prędkości magistrali, obecności pamięci wirtualnej i stronicowania). Dla tego względu należy dane przekopiować do obszaru urządzenia. Robotę wykonuje poniższy kod:

// Allocate the device input data, and copy the
// host input data to the device
CUdeviceptr deviceInputA = new CUdeviceptr();
cuMemAlloc(deviceInputA, numElements * Sizeof.FLOAT);
cuMemcpyHtoD(deviceInputA, Pointer.to(hostInputA),
        numElements * Sizeof.FLOAT);
CUdeviceptr deviceInputB = new CUdeviceptr();
cuMemAlloc(deviceInputB, numElements * Sizeof.FLOAT);
cuMemcpyHtoD(deviceInputB, Pointer.to(hostInputB),
        numElements * Sizeof.FLOAT);

// Allocate device output memory
CUdeviceptr deviceOutput = new CUdeviceptr();
cuMemAlloc(deviceOutput, numElements * Sizeof.FLOAT);

W linii 3ciej tworzymy wskaźnik, który może wskazywać na pamięć urządzenia. Następnie alokujemy do tego wskaźnika pamięć o wielkości numElements * Sizeof.FLOAT. Pierwsza wartość oznacza długość wektora, a druga wielkość zajmowanej pamięci przez jeden element. Sizeof.FLOAT wynosi 4 (bajty).

Następnie w linii 5tej i 6tej następuje kopiowanie ze „zwykłej” pamięci (host) do urządzenia (device), czyli metoda cuMemcpyHtoD(). Najpierw jako argument podajemy wskaźnik do pamięci urządzenia, później referencję dla naszego wektora z pamięci hosta. Na koniec jeszcze ilość bajtów, jaką należy skopiować. Analogiczne operacje są powtarzane dla drugiego argumentu wejściowego. Na koniec jest jeszcze alokowana pamięć na urządzeniu, gdzie będzie zapisywany wynik.

Dobra czas na przekazanie argumentów do funkcji. W nowszym API można to zrobić bardziej przyjemniej. Ja musiałem się zapytać na forum, co z tym fantem, ale na szczęście autor szybko odpisał. Rozwiązanie dla wersji 0.3.2a poniżej:

// sets parametrs
int offset = 0;

offset = align(offset, Sizeof.INT);
cuParamSetv(function, offset, Pointer.to(new int[]{numElements}), Sizeof.INT);
offset += Sizeof.INT;

offset = align(offset, Sizeof.POINTER);
cuParamSetv(function, offset, Pointer.to(deviceInputA), Sizeof.POINTER);
offset += Sizeof.POINTER;

offset = align(offset, Sizeof.POINTER);
cuParamSetv(function, offset, Pointer.to(deviceInputB), Sizeof.POINTER);
offset += Sizeof.POINTER;

offset = align(offset, Sizeof.POINTER);
cuParamSetv(function, offset, Pointer.to(deviceOutput), Sizeof.POINTER);
offset += Sizeof.POINTER;

Jako pierwszy argument przekazujemy do funkcji długość naszego wektora. Jest to liczba typu int i w linii 5tej właśnie ustawiamy, że numElements będzie przekazywane jako pierwszy argument. Jako że nie możemy utworzyć Pointer’a do pojedynczej zmiennej, trzeba zrobić na szybko jednoelementową tablicę i przekazać jako rozmiar wielkość int’a.

Od razu liczony jest offset, który w ostatniej metodzie danego listingu jest przekazywany do funkcji. Pewnie to jest po to, aby mechanizm wywołujący metodę, wiedział ile danych trzeba odczytać ze stosu (moje podejrzenie).

Dobra, mamy już dane wejściowe, mamy je przyporządkowane jako argument funkcji, najwyższy czas na wywołanie:

// Call the kernel function.
int blockSizeX = 256;
int gridSizeX = (int) Math.ceil((double) numElements / blockSizeX);

cuParamSetSize(function, offset);

// run grid
cuLaunchGrid(function, gridSizeX, blockSizeX);
cuCtxSynchronize();

Ustalamy wielkość bloku i gridu, który posłuży nam do obliczeń, przekazujemy długość parametrów i wywołujemy funkcję. O co chodzi z blokami i gridami to możecie doczytać w książce CUDA w przykładach  lub w CUDA C Programming Guide (obrazek 2-1, strona 9) który jest również dostępny po zainstalowaniu CUDA Toolkit.

Metodę cuCtxSynchronize() wywołujemy po to, aby w razie niepowodzenia jak najszybciej się dowiedzieć, gdzie mniej więcej jest błąd. Pozostaje jeszcze skopiowanie wyniku:

// Allocate host output memory and copy the device output
// to the host.
float hostOutput[] = new float[numElements];
cuMemcpyDtoH(Pointer.to(hostOutput), deviceOutput,
        numElements * Sizeof.FLOAT);

do RAMu, porównanie wyników:

// Verify the result
boolean passed = true;
for (int i = 0; i < numElements; i++) {
    float expected = i + i;
    if (Math.abs(hostOutput[i] - expected) > 1e-5) {
        System.out.println(
                "At index " + i + " found " + hostOutput[i] +
                        " but expected " + expected);
        passed = false;
        break;
    }
}
System.out.println("Test " + (passed ? "PASSED" : "FAILED"));

i sprzątanie:

// Clean up.
cuMemFree(deviceInputA);
cuMemFree(deviceInputB);
cuMemFree(deviceOutput);

No to czas na kod wykonywany na GPU:

extern "C"
__global__ void add(int n, float *a, float *b, float *sum)
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    while(i < n) {
      sum[i] = a[i] + b[i];
      i = i + blockDim.x * gridDim.x;
    }
}

Linijkę extern "C", potrzebujemy, aby kompilować naszą funkcję zgodnie z regułami kompilowania dla ANSI C. Następnie __global__ powoduje, że metoda oznaczona takim kwalifikatorem, będzie się wykonywać na GPU i może być wywołana z poziomu hosta (CPU). Inna użyteczna definicja to __device__. Funkcje oznaczone w ten sposób, również działają na GPU, ale mogą jedynie być wywołane z poziomu innych funkcji GPU.

Dalej mamy sygnaturę metody, czyli przekazujemy kolejno długość wektorów i wskaźniki na wektory wejściowe i jeden na wyjściowy. Kod jest podobny do tego przedstawionego w CUDA w przykładach (str. 66). W pierwszej linijce funkcji wyliczamy indeks, od którego będziemy zaczynać sumowanie. blockDim reprezentuje liczbę wątków w każdym wymiarze bloku. My stosujemy tylko jednowymiarowy blok. W blockIdx znajduje się indeks bloku, który aktualnie wykonuje kod danej funkcji, a w threadIdx indeks aktualnego wątku. Są to 3 standardowe zmienne systemu wykonawczego CUDA. Siatki bloków mogą być dwuwymiarowe, a każdy blok może mieć trójwymiarową tablicę wątków. Więc jak się ktoś orientuje w pięciu wymiarach, to ma pole do popisu.

Dalsze obliczenia (pętla while) wykonujemy od póki indeks jest mniejszy od długości zadanego wektora. W linijce 6tej nic się ciekawego nie dzieje (po prostu jest obliczany wynik) i w następnie obliczamy kolejny indeks. Normalnie (w przypadku implementacji jednowątkowej) to byśmy wykonywali inkrementację, ale jako że kod jest wykonywany na GPU, to kolejny indeks musimy trochę inaczej policzyć. Zwiększamy go o iloczyn liczby wątków na blok i liczby bloków w siatce.

Na razie to tyle co przygotowałem o cudach. Kod znajdziecie na githubie (JCudaExample), choć nie jest on nie wiadomo jak odkrywczy. Przy uruchomieniu mogą pojawić się kłopoty, więc warto dopasować ścieżkę (java.library.path) w konfiguracji uruchomieniowej.

Podsumowując, jeśli mamy sporo równoległych obliczeń do wykonania, dobrą kartę graficzną i zależy nam na szybkim czasie działania, to można się pobawić w Cuda. Nie jest to jednak tak przyjemnie jak JVM, gdzie wiadomo, z której linijki leci wyjątek i wszystko staje się szybciej jasne. Mimo wszystko praca ze sprzętem daje dużo satysfakcji.

środa, 15 sierpnia 2012

CUDA na Javie

Poniższy artykuł nie będzie ani o zjawiskach paranormalnych, „interwencji istot nadprzyrodzonych” (patrz: cud na Wikipedii) ani o śnieniu „na jawie” ;) Mianowicie CUDA (ang. Compute Unified Device Architecture) to opracowana przez Nvidię „uniwersalna architektura procesorów wielordzeniowych” występująca głównie na kartach graficznych. Architektura ta ma więc wspomagać rozwiazywanie skomplikowanych problemów numerycznych. Stworzono do tego framework OpenCL, który jest oparty na języku C (a dokładniej na C99). Jak słusznie zauważył Bartek, OpenCL jest osobną, alternatywą dla Cudy i nie będę się nią tutaj zajmował.

Przeglądając angielską wiki na temat CUDA, zauważyłem, że są dostępne 4 biblioteki dla Javy, współpracujące z technologią Nvidi: jCUDA, JCuda, JCublas, JCufft. Jako że moja karta graficzna (GeForce 9600M GT) w laptopie obsługuje CUDA w wersji 1.1 (aktualne dane można sprawdzić na stronie CUDA GPUs), postanowiłem trochę się pobawić tymi klockami i sprawić aby się choć trochę spociła.

Przyglądając się bliżej tym czterem bibliotekom Javowym, okazało się, że tak właściwie są to 2 projekty: jCUDA i JCuda. Pozostałe: JCublas i JCufft są de facto podprojektami JCuda. JCublas zajmuje się operacjami wektorowymi i macierzowymi. Natomiast JCufft zajmuje się szybką transformacją Fouriera. Ten kto kończył studia na Wydziale Elektroniki PWr, lub coś co jest związane z elektroniką, to wie o co chodzi ;) Druciarze rządzą!


Ściągnąłem więc na początek jCUDA wersję na Windowsa 64 bit.

Następnie utworzyłem projekt w IntelliJ Idea, skopiowałem zawartość katalogu examples (ze ściągniętego ZIP’a ) do src, a jcuda.jar, jcuda.dll i jcudafft.dll do lib i dodałem do projektu ze scope’m: "Compile"



Pierwsze uruchomienie klasy EnumDevices (widzę że jest tam jakiś main()) i dostajemy stacktrace’a:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no jcuda in java.library.path
 at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)
 at java.lang.Runtime.loadLibrary0(Runtime.java:845)
 at java.lang.System.loadLibrary(System.java:1084)
 at jcuda.driver.CUDADriver.(CUDADriver.java:909)
 at jcuda.CUDA.init(CUDA.java:62)
 at jcuda.CUDA.(CUDA.java:42)
 at examples.EnumDevices.main(EnumDevices.java:20)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Myślałem początkowo, że to podczas uruchomienia, aplikacja nie widzi dołączanych JAR’ów. Zmiana scope’a na Runtime, powoduje jednak, że IDEA nie widzi biblioteki podczas kompilacji:


Wróciłem wiec do zasięgu compile. Nie chcąc grzebać sobie zbytnio w classpath’ie, ustawiłem właściwość java.library.path w konfiguracji uruchomieniowej wspomnianej klasy na katalog z wymaganymi dll’kami:



Po ponownym uruchomieniu otrzymałem ten oto stos wywołań:

Exception in thread "main" java.lang.UnsatisfiedLinkError: D:\Java_programy\IdeaWorkspace\jCUDAFirstExample\lib\jcuda.dll: Can't load AMD 64-bit .dll on a IA 32-bit platform
 at java.lang.ClassLoader$NativeLibrary.load(Native Method)
 at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1803)
 at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1728)
 at java.lang.Runtime.loadLibrary0(Runtime.java:823)
 at java.lang.System.loadLibrary(System.java:1028)
 at jcuda.driver.CUDADriver.(CUDADriver.java:909)
 at jcuda.CUDA.init(CUDA.java:62)
 at jcuda.CUDA.(CUDA.java:42)
 at examples.EnumDevices.main(EnumDevices.java:20)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Hmmm, wersje systemu mam 64 bitową, ale nie na procku AMD tylko Intel Core 2 Duo T6400 2.00 GHz.

Dobra ściągamy wersję 32 bitową na Winde. Podmiana plików, przebudowa projektu i Voila:

Total number of devices: 1
Name: GeForce 9600M GT
Version: 1.1
Clock rate: 1250000 MHz
Threads per block: 512

W przykładowym kodzie znajduje się jeszcze klasa LoadModule, uruchamiam więc:

Exception in thread "main" CUDA Driver error: 301
 at jcuda.CUDA.setError(CUDA.java:1874)
 at jcuda.CUDA.loadModule(CUDA.java:375)
 at examples.LoadModule.main(LoadModule.java:36)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

I idę do linii gdzie poleciał wyjątek:

File cubinFile = new File("resources", "test_module.cubin");
cuda.loadModule(cubinFile.getAbsolutePath());

W archiwum z biblioteką jest jeszcze katalog resource, wiec kopiuję go do projektu i dołączam jako zasób:



Od razu nowe skróty klawiaturowe do zapamiętania, dostępne w oknie Project Structure - Modules - dodaj jako: Alt + S Sources, Alt + T Test Sources, Alt + E Excluded.

I otrzymujemy miły komunikat po uruchomieniu:

Test passed

Z ciekawości jeszcze zajrzałem co się znajduje w podpakiecie driver przykładowego kodu. Po uruchomieniu okazuje się, że aplikacje wypisują to samo na ekran. Porównałem kdiff3’em, co się zmienia w kodzie. Jak można przeczytać w release note w pliku readme.txt od wersji 1.1 biblioteki wprowadzono bardziej obiektowy sposób korzystania z biblioteki, aby nam Javowcom było łatwiej pracować :)

Added object oriented support for jCUDA and CUDA interface.

Niestety aby dowiedzieć się coś więcej o tej bibliotece, trzeba się zarejestrować na stronie producenta. Próbowałem z dwóch kont (gmail’owe i studenckie), ale zero odzewu. Później się doszukałem, że rejestracja jest tymczasowo nieaktywna i ze wyślą e-mail’a aktywacyjnego, gdy rejestracja będzie znów dostępna…

Jako, ze nie chcę pracować z biblioteką do której nie ma ogólnodostępnej dokumentacji, a rejestracja nic nie daje, wziąłem  się za drugą dostępną bibliotekę JCuda. Nauczony powyższym doświadczeniem od razu ściągnąłem wersję 4.0 32 bitową, dokumentację (są javadoc’i) i kod źródłowy. Początkowo postanowiłem przejrzeć przykłady: JCuda Code samples.

Zacząłem od pierwszego JCublasSample. Ściągnąłem, skopiowałem do src i to co było w ZIPie z JCudą skopiowałem do lib w moim projekcie. Następnie dodałem je jako zależności z zakresem Compile.


Pierwsze uruchomienie i oczywiście stacktrace:

Creating input data...
Performing Sgemm with Java...
Performing Sgemm with JCublas...
Error while loading native library "JCublas-windows-x86_64" with base name "JCublas"

Operating system name: Windows 7
Architecture         : amd64
Architecture bit size: 64
Stack trace from the attempt to load the library as a resource:
java.lang.NullPointerException: No resource found with name '/lib/JCublas-windows-x86_64.dll'
 at jcuda.LibUtils.loadLibraryResource(LibUtils.java:144)
 at jcuda.LibUtils.loadLibrary(LibUtils.java:80)
 at jcuda.jcublas.JCublas.initialize(JCublas.java:94)
 at jcuda.jcublas.JCublas.(JCublas.java:82)
 at JCublasSample.sgemmJCublas(JCublasSample.java:64)
 at JCublasSample.testSgemm(JCublasSample.java:49)
 at JCublasSample.main(JCublasSample.java:25)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Stack trace from the attempt to load the library as a file:
java.lang.UnsatisfiedLinkError: no JCublas-windows-x86_64 in java.library.path
 at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)
 at java.lang.Runtime.loadLibrary0(Runtime.java:845)
 at java.lang.System.loadLibrary(System.java:1084)
 at jcuda.LibUtils.loadLibrary(LibUtils.java:90)
 at jcuda.jcublas.JCublas.initialize(JCublas.java:94)
 at jcuda.jcublas.JCublas.(JCublas.java:82)
 at JCublasSample.sgemmJCublas(JCublasSample.java:64)
 at JCublasSample.testSgemm(JCublasSample.java:49)
 at JCublasSample.main(JCublasSample.java:25)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Exception in thread "main" java.lang.UnsatisfiedLinkError: Could not load the native library
 at jcuda.LibUtils.loadLibrary(LibUtils.java:122)
 at jcuda.jcublas.JCublas.initialize(JCublas.java:94)
 at jcuda.jcublas.JCublas.(JCublas.java:82)
 at JCublasSample.sgemmJCublas(JCublasSample.java:64)
 at JCublasSample.testSgemm(JCublasSample.java:49)
 at JCublasSample.main(JCublasSample.java:25)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Process finished with exit code 1

Z czego interesujące jest to:

no JCublas-windows-x86_64 in java.library.path

problem rozwiązuję jak w przypadku pierwszej biblioteki, czyli ustawiam właściwość java.library.path dla wirtualnej maszyny Javy w konfiguracji uruchomieniowej przykładu. Uruchamiam i ten sam błąd. Przyglądam się bliżej nazwie brakującego pliku: JCublas-windows-x86_64. Okazuje się, że w wskazanym katalogu mam taki plik ale bez przyrostka _64, czyli pewnie framework, na podstawie wyświetlanej wartości „Architecture: amd64” poszukuje automatycznie bibliotek 64bitowych. No dobra, ściągnąłem, podmieniłem, uruchamiam i znów myślałem, że dostałem to samo.

Jednak gdy się bliżej przyjrzałem, to się okazało ze jest trochę odmienny:

java.lang.UnsatisfiedLinkError: D:\Java_programy\IdeaWorkspace\JCudaExample01\lib\JCublas-windows-x86_64.dll: Can't find dependent libraries

Zajrzałem więc do JCuda Tutorial’a i tamta strona zaprowadziła mnie do JCuda FAQ na jakimś formu:

Type 2: UnsatisfiedLinkError: Can't find dependent libraries

Czyli jednak czegoś nie doinstalowałem związanego bezpośrednio z CUDĄ. Ściągnąłem więc CUDA Toolkit (64 bit) zainstalowałem i uruchomiłem ponownie komputer. Po odpaleniu aplikacji dostałem:

Creating input data...
Performing Sgemm with Java...
Performing Sgemm with JCublas...
testSgemm FAILED

Już jest lepiej, ale chyba coś dalej jest nie tak. Zajrzałem w kod i się okazało, że przykładowa aplikacja korzysta z biblioteki JCublas, a ta wykorzystuje cuBLAS (ang. CUDA Basic Linear Algebra Subroutines), czyli bibliotekę wspomagającą podstawowe obliczenia algebry liniowej. Nie chciało mi się zgłębiać co jest nie tak i uruchomiłem inny przykład.

Klasa JCufftSample wymaga do uruchomienia dodatkowo javovej biblioteki JTransforms, która to liczy m.in. transformatę Fouriera. Uruchomienie tego przykładu wykrywało w moim przypadku architekturę 32 bitową:

Operating system name: Windows 7
Architecture         : x86
Architecture bit size: 32

No i oczywiście sypało wyjątkami. Postanowiłem więc w katalogu lib trzymać zarówno 64 bitowe jaki 32 bitowe dll’ki. Prawdopodobnie dzięki temu udało mi się wywalić wirtualną maszynę:

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0ad2de60, pid=4376, tid=6112
#
# JRE version: 6.0_22-b04
# Java VM: Java HotSpot(TM) Client VM (17.1-b03 mixed mode, sharing windows-x86 )

Trochę zniechęcająca jest praca z tymi bibliotekami. Ale tak to zazwyczaj jest, jak się bawimy ze sprzętem, że na początku bardzo ciężko się z nim dogadać, a później to już jakoś lepiej idzie.

Jako że początkowo korzystałem z bibliotek w wersji 0.4.0-beta1 dla CUDA 4.0 postanowiłem zrobić downgrade do JCuda 0.3.2a. dla CUDA 3.2. Taka konfiguracja sprawia mi najmniej problemów. JCufftSample zaczął działać, i najprostszy, podstawowy test z tutorial’a też.
Proponuję wszystkim zaczynać od tego przykładu, gdyż ten test alokuje pewną pamięć w przestrzeni GPU i ją zwalnia – nic więcej.

Teraz to by było na tyle odnośnie cudów. Kolejne porady, co jeszcze należy doinstalować i jak przygotować środowisko do pracy z tym wynalazkiem w kolejnej części.

No i przepraszam za rozjechane listingi kodu pod Chrome'm, ale coś SyntaxHighlighter się pod tą przeglądarką rozjeżdża.

poniedziałek, 12 grudnia 2011

Agile Development Day

W sobotę 10 grudnia odbył się w Warszawie Agile Development Day. Była to impreza zorganizowana przez firmy Sages i Pragmatis. Event polegał na wspólnym tworzeniu aplikacji, ćwiczeniu TDD, programowania w parach i innych zwinnych praktykach. Był do tego Continous Integration (build był uruchamiany co git push) i Continous Delivery (deployment co udany build).

Aby dostać się na warsztaty trzeba było się wcześniej zapisać. Z ponad 60 osób chętnych, wybrano 22 w tym mnie :) Została przygotowana startowa baza kodu i udostępniona na github’ie. Trzeba było więc wcześniej ściągnąć kod i przygotować środowisko. Technologicznie był Spring Core, Spring MVC, JSP, Casandra, Maven. Jako że nie miałem wcześniej możliwości korzystać ze Spring’a w warunkach bojowych (po za jakimiś szkoleniami / prezentacjami), to miałem z czym się bawić. Zaprzyjaźniłem się więc z książkami na ten temat i archetypami kodu, aby porozkminiać te technologie i aby było na czym bazować.

Event zaczął się o 8 rano w Millennium Plaza. Najpierw było przywitanie i omówienie zasad, co będziemy robić w ciągu dnia. Następnie było o aplikacji jaką mamy stworzyć. Mianowicie za pomocą pisanego serwisu internetowego można się chwalić czego się nauczyliśmy, ile czasu na to poświeciliśmy i ile punktów zebraliśmy. Taki jakby twitter, tyle że piszemy co poznaliśmy, jaki był stopień trudności, system nalicza jakieś tam punkty i chwalimy się tym przed znajomymi. Plus jeszcze jakieś pierdoły do tego.

Po omówieniu wymagań aplikacji było wprowadzenie do Casandry, gdyż była to najbardziej „dziwna” i nieznana technologia z używanego technology stack. Później było szybciutko o Spring MVC i zaczęło się. Uczestnicy mieli wybrać sobie couch’a i osobę do pary (najlepiej aby ktoś znał Springa MVC). Jako że znałem Piotra (jednego z prowadzących) to się z nim dogadałem i wraz z Marcinem usiedliśmy do pierwszego user story.

Wybraliśmy sobie na tapetę formatowanie czasu, ile zajęła nam nauka danej technologii. Napisaliśmy kilka testów i zaczęła się zabawa. Postanowiliśmy dodać do projektu bibliotekę JUnitParams do pisania sparametryzowanych testów w JUnit’cie. Swoją drogą jest to biblioteka napisana przez Pawła Lipińskiego, który organizował, prowadził i kodował na warsztatach. Pomogło nam to pisać ładniejsze, parametryzowane testy w JUnicie, podczas pierwszej iteracji.

Wcześniej ustalono, że iteracje będą trwały po półtorej godziny i pomiędzy nimi będą zmiany par, a co kilka iteracji będzie retrospekcja. Czyli jakby jeden dzień ze sprintu został skompresowany do 1,5h a cały Sprint do 5ciu dni (czyli pięciu iteracji). Taka organizacja pracy bardzo motywowała do jak najszybszego kończenia zadań. Nie inaczej było w naszej parze, gdyż chcieliśmy jak najszybciej zobaczyć choćby częściowy efekt naszej pracy na „live”, mimo że nie wszystkie przypadki mieliśmy obsłużone. No i tu zaczęła się prawdziwa walka z technologiami.

Gdy jeszcze wczoraj Piotrek w projekcie dodał Tiles’y zaczęła się cała aplikacja wysypywać. Doszedł on do wniosku, że Casandra z Tiles’ami nie funkcjonuje ;) i wycofał zmiany. W naszej historyjce chcieliśmy (wręcz musieliśmy) skorzystać z JSTL’owego Taga c:forEach, ale też nie chciało działać. Był ten sam błąd co w przypadku Tiles’ów. Nasza koncepcja zamknięcia wyświetlania czasu we własny Tag JSTL’owy również nie zadziałała i trzeba było to jakoś inaczej obmyślić. To wszystko pewnie przez Casandrę ;)

Akurat nadszedł koniec pierwszej iteracji i nastąpiła rotacja par. Jako, ze pracowaliśmy na moim laptopie, to Marcin odszedł, a dołączył się Michał. Porozmawialiśmy chwilę, co zrobić z naszym problemem niemożliwości użycia JSTL’a i zaproponowałem, aby stworzyć Data Transfer Object (DTO) i za jego pomocą wyświetlać już odpowiednio sformatowany tekst. Coś tam niby się udało, ale potem było trochę walki z merge’owaniem zmian. Niestety szybko się okazało, że user story bardzo nachodzą na siebie, wręcz się powtarzają. Powodowało to masę konfliktów i trudności z push’owaniem zmian, gdyż po pull’u, odpaleniu testów, i przy próbie ponownego push’a okazywało się, że ktoś w między czasie zdążył wypushować swoje zmiany i proces trzeba było powtarzać ponownie.

Po drugiej iteracji postanowiono zrobić retrospekcję. Mieliśmy na małych, przylepnych karteczkach wypisać, co nam się podobało, a co nie i przykleić to na ścianę. Później trzeba było w ciszy to pogrupować (silent sorting) i ponazywać grupy. Po „uroczystym” przeczytaniu tego co jest fajne i niefajne, każda para wraz ze swoim mentorem miała się zastanowić, co zrobić, aby było lepiej. Super pomysłem było wprowadzenie stand-up meeting’ów - w trakcie trwania iteracji - [chyba] co 20 minut, aby poprawić komunikację w zespole i wiedzieć, kto co robi i na kiedy będzie. Innym ciekawym pomysłem było wpisywanie podczas commit'a / push’a jako autor numer implementowanego user story i nazwiska autorów. I rzeczywiście, początkowo brakło konwencji wypychania zmian do repozytorium, aby były one jednolite i czytelne.

Następnie był obiad i kolejna iteracja. Tym razem osoby, które przez dwie iteracje robiły jedną opowieść, miały się przesiąść. Może to trochę nieoptymalne posunięcie z punktu widzenia ukończenia zadania, ale podczas eventu chodziło o wspólną naukę. Trafiłem więc do zadania wyświetlania podpowiedzi. Mianowicie gdy wpisujemy technologię, której się właśnie nauczyliśmy, ma nam podpowiadać czy było to łatwe, średnie czy trudne. Wiązało się to z koniecznością napisania kawałka JavaScript’a, ale na szczęście to akurat poszło gładko. Problemem się jednak okazało, gdy coś co było zależne od naszego zadania (mianowicie dodawanie nowych umiejętności) czy jest już skończone i czy dane lądują tam, skąd je odczytujemy. Niestety ciężko w Casandrze cokolwiek podejrzeć, co wylądowało w bazie danych, gdyż zapisują się tam dane binarne. Nad tym zadaniem pracowałem przez dwie iteracje i później na ostatnią iterację, znów trzeba było się przesiąść.

Dosiadłem się więc do Bartka i tam nawet początkowo było fajnie (nie znaczy to wcale, że wcześniej było źle). Dostałem szybkie wprowadzenie co robimy, że właśnie jest test z nową funkcjonalnością i mam ją zaimplementować. Chwila ogarniania / czytania kodu i z pomocą Bartka udało się. Problem niemożliwości skorzystania z JSTL’a rozwiązano użyciem Skryptletów. Coś tam nawet się udało wyświetlić, ale nie można było wypchać zmian, gdyż najpierw trzeba było powalczyć  z kwestią, co wyświetlać, jak ktoś jest niezalogowany. Inny zespół pisał back-end do tej funkcjonalności i niestety czas warsztatów nie pozwolił już na połączenie naszych zmian.

Po piątej iteracji przejrzeliśmy backlog zapisywany na tinypm.com. Okazało się, że niewiele funkcjonalności udało się w pełni zaimplementować. Paweł pokazał jednak na żywym środowisku, że coś tam działa. Patrząc na statystyki na Jenkinsie, było widać, że w ciągu zmian udało się wypchnąć do repozytorium kod 54 razy z czego tylko 9 się wywaliło. Uważam to za niezły wynik, biorąc pod uwagę to, że dla uczestników był to świeży projekt, bez wypracowanych konwencji i nie każdy musiał znać wszystkie wykorzystywane technologie. Poniżej przedstawiam trend tesów.


Podczas warsztatów budowały się build’y od 88 do 142. Wcześniejsze build’y pochodziły z przygotowań do warsztatów. Widać wyraźnie że od 95 build’a liczba testów zaczęła znacząco rosnąć, z każdą nową zmianą.

Na koniec nadszedł czas na podsumowanie. Poprzestawialiśmy trochę stoły i ułożyliśmy krzesła w dwa kółka. W środku było 5 krzeseł, a  na zewnątrz reszta. Kto siedział w środku ten mógł mówić, pod warunkiem, że cztery wewnętrzne krzesła były zajęte. Jedno było wolne, aby kogoś mógł się dosiąść i oczywiście można było spokojnie opuścić wewnętrzny krąg. Osoby siedzące w środku mówiły o swoich odczuciach dotyczących warsztatów, co się nauczyły itd. Czyli taka runda feedback’owa dla innych uczestników i organizatorów.

Jako że ja nie miałem okazji zasiąść w środku, to swoją opinię wyrażę teraz. Ogółem event wyszedł bardzo fajnie. Organizacja w porządku: były kanapki rano, napoje i ciasteczka przez caly dzień, dobry obiad w trakcie (i nie była to pizza). Lokalizacja warsztatów bardzo dobra, mianowicie blisko centralnego – ważna kwestia dla osób przyjezdnych, a tych nie brakowało. Ja sam przyjechałem z Wrocławia, ale spotkałem również ludzi z Gdyni, Gdańska, Wrocławia, a nawet z Berlina. Organizacja pod względem technicznym również dobra – były dwa rzutniki, na jednym Jenkins, a na drugim live aplikacja. Bardzo dobrym pomysłem było przygotowanie zaczynu aplikacji i jej udostępnieniu w necie. Również Continous Integration i Delivery daje bardzo fajny efekt psychologiczny. Można było szybko zobaczyć swoje zmiany na Jenkinsie jak i na na środowisku „produkcyjnym”.

Niestety, jak to sami organizatorzy przyznali, rozpoczęcie przygotowywania tej aplikacji na tydzień przed warsztatami to było trochę za późno. Wynikały później z tego problemy z technologiami (niemożliwość użycia Tiles’ów i JSTL’a). Uważam również, że dobranie stosu technologicznego było nienajszczęśliwsze. Fajnie, że było cos egzotycznego (Casandra), ale mało kto wiedział, jak z tego odpowiednio korzystać. Do tego zastosowanie Springa MVC i JSP powodowało, że czasem trzeba było się męczyć z JavaScriptem (AJAX i jQuery). Te problemy technologiczne powodowały, ze ciężko było się skupić na czystym pisaniu kodu (nie wspominając już o TDD), a trzeba było się męczyć ze zgraniem technologii i merge’owaniem zmian.

Wcześniej wydawało mi się że w miarę ogarnąłem już Git’a, ale dopiero tutaj w warunkach bojowych zweryfikowałem swoje poglądy na ten temat. Problem scalania zmian występuje (pushowaliśmy do jednej gałęzi, aby Continous Server już dalej robił swoje), jak w każdym innym systemie kontroli wersji. Jakub Nabrdalik mówił, że jednym z możliwych sposobów jest zrobienie miecza z dwóch zwiniętych kartek papieru i ten kto aktualnie posiada miecz może push’ować. Jak ktoś innyc chce to robić, to musi uzyskać ten miecz. Wydaje mi się, że skorzystanie z Mercuriala mogło by trochę ułatwić sprawę z łączeniem zmian w kodzie, ale nie jestem ekspertem w tym temacie.

Innym poważnym minusem co do warsztatów, było przygotowanie user story’s. Bardzo często były one do siebie podobne, zachodziły na siebie i od siebie zależały. Jednak podczas wyboru user story nie posiadaliśmy tej wiedzy. Na przyszłość trzeba lepiej przygotować historyjki do zaimplementowania, wraz z grafem, co od czego zależy, co trzeba zrobić najpierw i z lepszym opisem. Większość opowiastek dotyczyła strony głównej, przez co wzajemnie sobie modyfikowaliśmy zawartość tych samych plików, co prowadziło do problematycznych konfliktów. Na przyszłość można kilka opowiastek udostępnić np. tydzień wcześniej, aby można było w domu coś na rozgrzewkę napisać. Zachęciło by to bardziej do zajrzenia w kod i do lepszego przygotowania do warsztatów. I najlepiej aby były one możliwie jak najbardziej rozłączne.

Również wybór Casandry na bazę danych nie było chyba najlepsze, gdyż z rozmów pomiędzy ludźmi słyszałem, że niektóre zespoły sporo się z nią męczyły. Fajnie poznać przy takiej okazji coś nowego, ale wybór chyba nie był najszęśliwszy. Jak bym proponował na następny raz jakąś bazę działającą w pamięci, inną NoSQL’ową (obiektową, key-value) lub jakąś lekką SQLową, którą wszyscy znają. Jako technologię widoku proponowałbym GWT (lub coś o podobnej koncepcji), gdyż tam nie trzeba się bawić z JavaScriptem, ani z JSP, a maksymalnie z niewielkimi kawałkami HTML’a.

Nie oznacza to, ze nic nie wyniosłem z warsztatów. Miałem w końcu motywację do lepszego poznania Springa, Git’a, JUnitParams… Zrozumiałem, że nie tylko testy jednostkowe są najważniejsze, ale obok nich musi się znajdować kilka integracyjnych i akceptacyjnych (już w początkowym stadium projektu). Przykładowo prosty test end-to-end uruchamiający przeglądarkę na stronie głównej i sprawdzający czy jakiś tam przycisk istnieje, pozwolił nam szybko wykryć, że Tagi w JSP nie chcą działać. Inna sprawa ze testy nie były podzielone na jednostokowe i inne (trwające dłużej), przez co ich uruchamianie było trochę przydługie.

Dowiedziałem się też o fajnej technice, podkreślającej współwłasność kod. Mianowicie po za wywaleniem z szablonów plików / klas i metod informacji o autorze, można dać wszystkim pracownikom z danego projektu, jedno konto i hasło do repozytorium. Dopiero wtedy powstaje wspólny kod i nikt się nie boi modyfikować nieswoich części. Poznałem również trochę bardziej w praktyce procesy zachodzące w procesie Agile'owym, jak i mogłem się dowiedzieć jak wygląda IT w innych miastach.

Zawsze z takich wydarzeń można wynieść ciekawe skróty klawiaturowe. I tak dla IDEI: Crtl + Shift + Enter - Statement Comlete, czyli dodanie średnika na końcu linii. Z analogicznych skrotów korzystałem w NetBeans’ie, ale tam to było to Ctrl + ; i Ctrl + Shift + ;. Innymi interesującymi skrótami w Idei są: Ctrl + Shift + T czyli przejście do klasy, która testuję aktualną klasę, w której jesteśmy i Ctrl + Shift + N (odpowiednik Eclipsowego Ctrl + Shift + R), czyli wyszukiwanie po nazwie pliku z projektu.

Ciekawe, nowopoznane skróty dla Eclipse’a to Ctrl + Shift + M - zaimportowanie statyczne danej metody na której znajduje się aktualnie focus. Przydatne zwłaszcza jak mockujemy z Mockito i chcemy samo when() zamiast Mockito.when(). Ponadto Ctrl + Shift + O czyli organizacja importów (w Idei można zrobić, aby automatycznie narzędzie przed commitem zmian, odpowiednio organizowało importy). I na koniec syso + Ctrl + Spacja, czyli skrót od System.out.println();. W Idei jest to sout + Tab co jest według mojego gustu fajniejsze.

Dobra to chyba tyle z podsumowania wydarzenia. W tym roku już się żadna konferencja / warsztaty nie szykują, więc trzeba będzie z tym poczekać do przyszłego roku. Mam nadzieję na kolejną edycję Agile Development Day lub podobny tego typu event.

wtorek, 7 czerwca 2011

Zmiana nazwy pakietu w projekcie Android'owym w IDEA

W jednym z ostatnich wpisów: Inne podejście do przechowywania danych w systemie Android, z wykorzystaniem db4o i NetBeans 6.9 wspominałem o użyciu IntelliJ IDEA w celu sprawdzenia jak sobie radzi z aplikacjami android’owymi. I rzeczywiście wsparcie jest lepsze. Póki co to natrafiłem tylko na dziwne zachowanie jeśli chodzi o zmianę nazwy pakietu. Mianowicie po wybraniu pakietu i Refactor -> Rename (Shift + F6) wyskakuje mi poniższy monit:



Z początku trochę mnie zdziwił komunikat. Patrząc tylko na przedostatnią linię, nie rozumiałem intencji. Przecież pakiet jest utożsamiany z katalogiem, więc o co chodzi? Bliższe wczytanie się w komunikat, już wyjaśnia co się dzieje. Otóż IDEA umieszcza pliki automatycznie generowane (takie jak klasa R) w osobnym katalogu (/gen). Niezależnie od wybranej opcji (po za Cancel oczywiście) zmieniana jest nazwa katalogu zawierającego nasze pliki źródłowe. Przy wyborze Rename package, dodatkowo zostanie zmieniona nazwa podkatalogu /gen, gdzie są generowane automatycznie pliki projektu (np. klasa R).

Teraz przy próbie uruchomienia (Shift + F10) aplikacji wyskakuje okienko do edycji konfiguracji.



Możemy w tym momencie olać ten ekran i samemu poprawić ścieżki do naszych aktywności w pliku AndroidManifest.xml. Całe szczęście, że IDEA podświetla na czerwono błędne klasy, umieszczone w plikach XML-owych. O takiej funkcjonalności w NetBeans’ie można tylko pomarzyć.

Kilkakrotne próby powtórzenia tej czynności, uświadomiły mi, że środowisko nie zawsze tak samo wszystko modyfikuje. Raz musiałem w jakimś pliku konfiguracyjnym coś grzebać, a ostatnio nawet udało się odpalić od razu aplikację. Czyżby to było adaptacyjne środowisko programistyczne? Jeśli tak to jestem pod wrażeniem.

sobota, 7 maja 2011

Inne podejście do przechowywania danych w systemie Android, z wykorzystaniem db4o i NetBeans 6.9

W systemie Android domyślnie mamy dostęp do relacyjnej bazy danych SQLite w której możemy zapisywać informacje z naszej aplikacji. Jest to typowy sposób dostępu do danych w tym systemie. Nie jest dziwne, że do tego systemu została wybrana ta baza. Nie posiada ona wielu magicznych dodatków (jak obecnie popularne kombajny) i założenia jest „Lite” (przynajmniej z nazwy). Z innej strony jest to baza o ciekawym stosunku kodu źródłowego do testów. Odsyłam do artykułu: Extremalne testowanie w SQLite Pawła Lipińskiego. Jak skorzystać z tej bazy jest już opisane na blogu Mirosława Stanka (część 1, część 2, część 3) jak i na helloandroid.  Ja nie będę po raz kolejny opisywał tego rozwiązania, ale postaram się poniżej pokazać inne (ciekawsze?) podejście do tematu, z wykorzystaniem obiektowej bazy danych.

DataBase For Objects, zwana powszechnie db4o, to interesująca alternatywa dla SQLite w systemie Android, jeśli chodzi o składowanie danych. Od wersji 7.5 biblioteka daje wsparcie dla tej platformy. Poniżej przedstawię jak zbudować projekt w NetBeans’ie 6.9 z wykorzystaniem tej biblioteki.

Tworzymy w NetBeans’ie nowy projekt typu AndroidApplication. Następnie do bibliotek dodajemy JAR’a zawierającego db4o.


Następnie warto skorzystać z klasy Db4oHelper przedstawionej przez Damasia Maneiro (niestety nie mam pojęcia jak to po polsku odmienić) w artykule na dzone.com: Using db4o in an Android application. Jest tam sporo kodu odwołującego się do klas z tamtejszego projektu, więc pozwoliłem sobie trochę ten kod zmienić do naszych potrzeb. Listing poniżej.

public class Db4oHelper {

    private static ObjectContainer oc = null;
    private Context context;

    public Db4oHelper(Context ctx) {
        context = ctx;
    }

    public ObjectContainer db() {
        try {
            if (oc == null || oc.ext().isClosed()) {
                oc = Db4oEmbedded.openFile(dbConfig(), 
                        db4oDBFullPath(context));
            }
            return oc;
        } catch (Exception ie) {
            Log.e(Db4oHelper.class.getName(), ie.toString());
            return null;
        }
    }

    private EmbeddedConfiguration dbConfig() throws IOException {
        EmbeddedConfiguration configuration = 
                Db4oEmbedded.newConfiguration();
        return configuration;
    }

    private String db4oDBFullPath(Context ctx) {
        return ctx.getDir("data", 0) + "/" + "database.db4o";
    }

    public void close() {
        if (oc != null) {
            oc.close();
        }
    }
}

Mamy do dyspozycji publiczny konstruktor, który jako argument przyjmuje Context – podobnie jak w SQLite. Potrzebne nam to będzie do umieszczenia pliku z bazą danych w miejscu, gdzie znajdują się dane aplikacji. Następnie mamy metodę db(), która zwraca nam instancję klasy odpowiedzialnej za połączenie z bazą. Mamy jeszcze metodę zwracającą nam konfigurację bazy (domyślna nam początkowo wystarczy) i ścieżkę do pliku z bazą. Na koniec metoda do zamknięcia połączenia z bazą.

Teraz można już napisać klasy, które chcemy składować w bazie danych. Mamy kilka możliwości jak to zrobić. Ja poniżej przedstawię prosty obiekt POJO i odpowiadającą mu klasę DAO. Można oczywiście to wszystko w jedną klasę wrzucić, ale tak będzie ładniej i poprawniej. Poniżej przykładowy kod klasy User.

public class User {

    private String login;
    private String password;

    public User(String login, String password) {
        this.login = login;
        this.password = password;
    }

    public String getLogin() {
        return login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

W klasie są definiowane 2 pola typu String o nazwach login i password oraz odpowiadające im gettery i settery. No i jeszcze konstruktor przyjmujący te parametry. Jeśli ktoś woli programować w stylu funkcyjnym, to można zrobić pola final i wyrzucić settery. Db4o i tak zadziała, gdyż do ustawiania wartości wykorzystuje refleksje do pól składowych, a nie do setterów.

Proszę zwrócić uwagę, że klasa nie posiada żadnego pola w stylu ID. Dodawanie do klas persystentnych identyfikatorów, to niemalże konieczność w świecie relacyjnym, gdyż dzięki identyfikatorom tworzy się relacje i złączenia przy zapytaniach. W bazach obiektowych tego nie potrzebujemy! Mamy przecież referencje i kolekcje.

Poniżej kod klasy DAO dla klasy User:

public class UserDao {

    private ObjectContainer db;

    public UserDao(Context context) {
        Db4oHelper db4oHelper = new Db4oHelper(context);
        this.db = db4oHelper.db();
    }

    public void saveOrUpdate(User user) {
        db.store(user);
    }

    public void delete(User user) {
        db.delete(user);
    }

    public List<User> getAll() {
        return db.query(User.class);
    }
    
    public List<User> getByLogin(String login) {
        User example = new User(login, null);
        return db.queryByExample(example);
    }

    public List<User> getByLoginAndPassword(
                 String login, String password) {
        User example = new User(login, password);
        return db.queryByExample(example);
    }
}


Klasa posiada konstruktor, za pomocą którego do klasy przekazujemy obiekt typu Context. Jest to klasa android’owa, za pomocą której Db4oHelper może się dostać do odpowiedniego miejsca na zapisanie pliku z bazą. Następnie pobieramy i zapamiętujemy obiekt typu ObjectContainer pochodzący z biblioteki db4o i za jego pomocą odbywa się dostęp do danych. Dalej mamy operacje tworzenia, modyfikacji, usuwania i pobierania wszystkich obiektów - nic specjalnego, ot proste wywołanie pojedynczych metod.

Trochę (ale tylko troszkę) ciekawsza jest metoda getByLogin(), która korzysta z query by example. W skrócie, ta metoda wykonywania zapytań, polega na utworzeniu najpierw obiektu, który będzie przykładem, a następnie wywołaniu odpowiedniej metody zwracającej wszystko, co do danego wzrorca pasuje. Domyślne wartości pól klasy są dopasowywane dowolnie, a te zmodyfikowane dokładnie. W tym przypadku zostaną zwrócone obiekty posiadające ustawione pole login na takie samo jak przekazane w argumencie metody. No prościej się chyba nie da. Istnieją w db4o jeszcze inne bardziej wyrafinowane metody zapytań, ale to nie temat na ten wpis.

Ludzie doświadczeniem z dużymi systemami i relacyjnymi bazami danych, widząc powyższy kod, zaraz zaczną krzyczeć: „a co z dostępem równoległym?”, „a gdzie początek i koniec transakcji?”, itd. Otóż w ogólności db4o wspiera ACID, a poziom izolacji transakcji ma ustawiony na Read Committed. Pytanie z mojej strony: czy rzeczywiście wszędzie tego potrzebujemy? Z tego, co mi wiadomo, to zdarzenia z aktywności w Androidzie są obsługiwane synchronicznie, tzn. w tym samym wątku co GUI. Jeśli nie tworzymy dodatkowych wątków do obsługi zdarzeń (ze względu na ich banalność i szybkość), to nie potrzebujemy transakcji.

Dobra, to teraz by się przydało jakieś GUI dorobić, aby można było zobaczyć jakiekolwiek efekty działania. Po uruchomieniu aplikacji będą się pojawiać dwa przyciski: Create New User i Login. Te będą kierować już do następnych ekranów. Tam będzie można utworzyć nowego użytkownika (i oczywiście go zapisać w bazie), albo się zalogować do aplikacji (na podstawie loginu i hasła zapisanego w bazie). Nie pokazuję tutaj jak utworzyć GUI, gdyż nie jest to tematem tego artykułu. Całkowity kod zamieszczam na końcu wpisu.

Przy pierwszej próbie uruchomienia tak skonstruowanej aplikacji dostaniemy błąd. Jeżeli nie zepsuliśmy niczego po drodze to problem jest jasny. Samo dołączenie biblioteki db4o do projektu nie spowoduje jej dołączenia w trakcie budowy projektu. Należy jeszcze dodać do build.xml następujący target (lub podobny):


    
        
    


Uważam, że środowisko programistyczne powinno być na tyle inteligentne, aby się troszczyło za nas takimi detalami. No ale cóż, nie można mieć wszystkiego. Teraz już powinno pójść gładko i jeśli nic nie popsuliśmy w XML’ach to powinniśmy zobaczyć naszą aplikację. Przykładowe screeny poniżej:



Pod koniec chcę zaznaczyć, ze NetBeans 6.9 nie jest idealnym środowiskiem do pisania aplikacji androidowych (może trzeba jeszcze poczekać?). W trakcie pracy nad projektem natrafiłem na sporo utrudnień. Przykładowo, podczas refaktoringu, po przeniesieniu klasy aktywności, aplikacja przestawała działać. Trzeba było wówczas w AndroidManifest.xml trochę pogrzebać i poprawić nazwy klas. Również zmiana nazwy pakietu wymuszała dodatkowe grzebanie w project.properties.

Z ciekawości sprawdziłem jeszcze jak sobie ze wsparciem Androida radzi IntelliJ IDEA (tak w końcu ją zainstalowałem :) ). Tutaj przeniesienie klasy do innego pakietu nie stanowi problemu. Podobnie z dodaniem biblioteki db4o do projektu. Co do zmiany nazwy pakietu to napiszę o tym w następnym poście (aby wszystko na raz nie było w jednym). Dodatkowo Idea od razu pokazuje Log konsolę z Androida. Chcą ją aktywować w NetBeans'ie trzeba kliknąć: Window -> Output -> ADB Log.

Jeszcze w między czasie tworzenia tego wpisu (nie powstał on w jeden dzień) pojawił się NetBeans 7.0. Tam jednak nawet nie mogłem dodać żadnej zewnętrznej biblioteki do projektu android'owego, więc ewentualna migracja do nowego środowiska musi poczekać.

Link do kodu:

db4oOnAndroid.zip

git://github.com/mstachniuk/db4oOnAndroid.git

//Update (25.09.2011)
Jak słusznie zauważył mój przyjaciel Michał, skoro otwieramy połączenie do bazy danych, to musimy je również zamknąć. Można to zrobić w MainActivity.java w przeciążając metodę onDestroy():

@Override
protected void onDestroy() {
    Db4oHelper db4oHelper = new Db4oHelper(this);
    db4oHelper.close();
    super.onDestroy();
}