## Treść Treść zadania w pliku `tieto_zadanie_rekrutacyjne_cut_v2.pdf` ## Kompilacja System budowania to CMake, kompilacja w następujących krokach: ``` mkdir build && cd build cmake .. cmake --build . ``` Pliki wynikowe będą w katalogu `bin`. Plik aplikacji to `cpu-tracker`, reszta to testy. Wszystkie testy można uruchomić pojedynczo lub zbiorczo przez CTest, uruchamiając polecenie `ctest` w katalogu `build` lub polecenie `ctest -V`, aby zobaczyć wyniki pośrednie i wartości zmiennych w przypadku niezaliczonego testu. ## Projekt Rozwiązanie w tym repozytorium to mocny over-engineering, ale przyjąłem, że takie były oczekiwania zważywszy na polecenie odczytu danych w pięciu wątkach. W rzeczywistym przypadku zrealizowałbym to z jedną kolejką zadań pracującą w głównym wątku, plus drugi wątek (worker) w tle, który wykonuje odczyt i przerzuca kopię danych na kolejkę w wątku głównym. ### Struktura projektu W projekcie wydzieliłem dwie biblioteki, które mogłyby mieć duży re-use w kolejnych projektach. Są to `log` do logowania z podaniem formatu (jak w printf) i `thread` przykrywający `pthread` i dodający funkcje i typy pomocnicze (kolejka, bloker, watchdog itp.). Cała reszta, czyli główna aplikacja jest w katalogu `app`. Każda część (moduł, biblioteka, aplikacja) zazwyczaj jest podzielona tak samo, chyba że jest bardzo prosta. Podział wychodzi głównie z koncepcji Hexagonal Architecture i Clean Architecture (i może lekko DDD - Domain Driven Design, linki poniżej). Ten podział to: `core` (lub np. `domain`, czasami `entities`) - czysty język programowania do zapisu idei związanych z dziedziną problemu, logiką biznesową itp. Wszystko, co można omówić w ekspertem i zapisać w pseudokodzie. Jeśli potrzebny jest np. zapis do pliku, odczyt godziny, pobranie danych itp. używana jest abstrakcja w postaci np. wywołania funkcji spod wskaźnika (który zostanie dostarczony podczas inicjalizacji aplikacji). Jeśli aplikacja jest duża i dobrze opisana, dodaję tu katalog `use_cases` lub `commands`, gdzie są obiekty lub funkcje realizujące wszystkie przypadki użycia danej aplikacji. `application` - logika aplikacji, czyli np. obsługa zdarzeń i warunkowe sterowanie przepływem, odpalanie funkcji z innych warstw itp. Jeśli jest tego mało, pomijam i rzucam do warstwy `main` (jak w przypadku tego projektu). Ten kod może zależeć tylko od `core`. `interface` (lub czasami `controllers`, `adapters`, `gateways` lub wszystko to jako podkatalogi w `interface`) - jeśli aplikacja ma dużo interfejsów wejścia (np. CLI, HTTP api, GUI, MQTT itd.), to ten katalog zawiera "wejścia", czyli zestawy funkcji dostępowych do domeny oraz "prezentery" tłumaczące dane na format prezentacji, np. json, xml, html itd. Tutaj był tylko prosty printer z jedną funkcją, więc darowałem tę część i wrzuciłem go do `infrastructure`. Ten kod może zależeć od mniejszych bibliotek w części implementacji (raczej nie w publicznych interfejsach), ale raczej nie od frameworków i konkretnych technologii (np. bazy danych). `infrastructure` - kod zależny od technologii, systemu operacyjnego, podatny na częste zmiany techniczne, np. po cotygodniowych spotkaniach zespołu. Tutaj wchodzi implementacja zapisu/odczytu danych, połączeń z innymi urządzeniami, komponentami sprzętowymi itp. Jeśli core musi użyć np. dostępu do stałych danych czy odczytać czas systemowy, tutaj jest to implementowane, a wskaźnik zostanie wstrzyknięty do core w warstwie main. `main` - kod składający wszystko w całość, tworzący obiekty z innych warstw, uruchamiający wątki, zwalniający pamięć po zamknięciu itp. To "brudny" kod, który może zależeć od wszystkiego, ale nie powinien zawierać za dużo logiki aplikacji, i definitywnie żadnej logiki biznesowej. `tests` - katalog z testami. `tests/unit` - testy jednostkowe, zazwyczaj nie linkują całych bibliotek, testują pojedyncze funkcje lub powiązane zestawy funkcji. Implementują mocki/stuby/fake'i i wstrzykują do testowanych jednostek aby zrealizować scenariusz testowy. `tests/integration` - testy sprawdzające działanie więcej niż jednego komponentu lub pojedynczych komponentów, ale z wykorzystaniem np. systemu operacyjnego (czas, pliki, wątki itd.). `tests/functional` - testy uruchamiane przez zewnętrzny framework weryfikujący wyniki danego zakresu funkcjonalnego. Dodatkowo w katalogu głównym projektu może znaleźć się katalog `extern` z bibliotekami zewnętrznymi. W tym projekcie korzystałem z `greatest` do testów, ale zastąpiłem go własnym makrem `CHECK`. ### Testy Wymagany był przynajmniej jeden test, tutaj zaliczam do niego głównie testy jednostkowe kolejki zadań w bibliotece `thread`. Można dopisać tam jeszcze kilka przypadków opisujących specyfikację, np. przypadek sprawdzający, czy po zatrzymaniu kolejki zostaną wykonane zaległe zadania czy może kolejka je usuwa. W testach integracyjnych często nie robiłem asercji, służyły mi do sprawdzania wyników w trakcie pisania, a CTest i tak weryfikuje test na podstawie kodu na wyjściu (0 zaliczony, inaczej niezaliczony). ### Linki Hex: https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture/ https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c Clean: https://betterprogramming.pub/the-clean-architecture-beginners-guide-e4b7058c1165 https://codilime.com/blog/clean-architecture/ SOLID + wzorce: https://www.renaissancesoftware.net/files/articles/ESC-204Paper_Grenning-v1r0.pdf https://media.readthedocs.org/pdf/eswp3/v1.0/eswp3.pdf Dobry e-book o wzorcach w C dla systemów embedded: https://repositorio.uci.cu/jspui/bitstream/123456789/10139/1/Design%20Patterns%20for%20Embedded%20Systems%20in%20C_%20An%20Embedded%20Software%20Engineering%20Toolkit%20%28%20PDFDrive%20%29.pdf ### Realizacja W najkrótszym wariancie można zrealizować przerzucanie danych między wątkami z mutexami i zmiennymi warunkowymi (np. `pthread_cond_t`). Tutaj trudno byłoby jednocześnie spełnić dydaktyczne wymogi zadania, więc całość działa na kolejkach zadań (opisane m.in. w e-booku linkowanym powyżej). Kolejka zadań zawiera dynamiczną kolejkę FIFO wskaźników na funkcje `void(void*)`. Po wystartowaniu działa w pętli, sprawdzając, czy w kolejce czeka coś do wykonania. Jeśli tak, uruchamia funkcję spod wskaźnika i zdejmuje wskaźnik z kolejki. Każdy wątek ma swoją kolejkę (`TaskQueue`). W standardowym przypadku zadania, które mogą powodować równoczesny dostęp do zasobu są kolejkowane w jednym wątku. Tutaj np. dzięki kolejce z zadaniami wyświetlania komunikatów albo logowania do pliku, nie będzie śmieci w postaci pomieszanych linii czy wyrazów, kiedy wiele wątków próbowałoby pisać jednocześnie. `Task` to po prostu wskaźnik na funkcję `void(void*)`. Parametrem `void*` może być wszystko, np. liczba, struktura lub kolejna funkcja do wykonania. Jednostka `Thread` przykrywa `pthread`, zgodnie z techniką/idiomem `pimpl`. Taki wrapper uwalnia użytkowników biblioteki (tutaj główną aplikację) od wiedzy o użytej technologii/frameworku/bibliotece i nie wprowadza bezpośredniej zależności (np. nie trzeba aktualizować plików nagłówkowych, jeśli zmieni się implementacja). Jednostka `QueuedThread` ułatwia tworzenie wątków z wbudowaną kolejką zadań, aby nie trzeba było składać wszystkiego każdorazowo dla nowego wątku. `Blocker` to wrapper zmiennej warunkowej (`pthread_cond_t`). Pozwala na czasowe blokowanie wątku do momentu odblokowania przez inny wątek. W najprostszej wersji można użyć `sleep`, ale wtedy: a) trzeba doczekać, aż sleep skończy czekać aby przejść dalej i np. zdjąć zadania z kolejki lub wyłączyć program; b) trudno pisać testy jednostkowe bez możliwości wstrzykiwania funkcji czekającej do testowanej jednostki. `Watchdog` posiada listę wątków, którymi się "opiekuje". Bardziej zgodne z regułą SRP byłoby tworzenie wątków poza watchdogiem i późniejsze rejestrowanie ich w nim. Tutaj ze względu na mały kod, umieściłem tworzenie i usuwanie wątków w watchdogu. Dzięki zastosowaniu kolejek, implementacja watchdoga jest prosta i nie wymaga "zatruwania" wątków jakimiś mechanizmami powiadamiania go o tym, że jeszcze żyją. Skoro każdy wątek i tak ma kolejkę, watchdog po prostu wrzuca im zadanie do wykonania, one nawet nie wiedzą, co wykonują. Zadanie to zaktualizowanie aktualnej godziny w watchdogu. Watchdog sprawdza okresowo, czy wszystkie wpisy dla wątków są aktualne, jeśli któryś wątek nie wykonał w wyznaczonym czasie aktualizacji, watchdog uznaje, że się zawiesił. UWAGA DO WYNIKÓW: odczyt i wyliczanie użycia CPU to implementacja z pierwszego lepszego artykułu. Empirycznie wydaje się działać dobrze, ale zauważyłem, że po przełączeniu komputera w stan uśpienia i wybudzeniu go po kilku godzinach, wartości dla wszystkich rdzeni mają 99% i maleją z każdą minutą. Nie debugowałem tego, bo to skrajny przypadek i nie jest to faktycznym celem zadania. UWAGA DO WYDAJNOŚCI: w systemach embedded lub systemach czasu rzeczywistego sporo z tego byłoby do skrócenia. Wtedy kosztem abstrakcji minimalizowałbym liczbę dereferencji wskaźników lub zamiast wstrzykiwania zależności używałbym importu funkcji przez `extern` w plikach `.c`.