Componenti:
- Manuel Andruccioli, manuel.andruccioli@studio.unibo.it
- Kelvin Olaiya, kelvinoluwada.olaiya@studio.unibo.it
L'obiettivo è quello di affrontare il problema descritto nell'assignment 1, utilizzando quattro diversi approcci di programmazione asincrona, descritti di seguito.
Si è deciso di uniformare l'interfaccia degli approcci, sfruttando opportune astrazioni, sia per la versione CLI, che per la versione GUI.
interface SourceAnalyzer {
Future<Report> getReport(Directory directory);
ObservableReport analyzeSources(Directory directory);
}
Nel caso di getReport(Directory directory)
, verrà restituita una Future, che sarà completata, in modo asincrono, con il report dell'analisi.
Questo permette, in tutti i casi, di sottomettere la computazione e poter successivamente attendere, bloccando opportunamente il Thread che vuole ottenere il risultato.
Un approccio alternativo è quello di aggiungere un Runnable al metodo, che verrà eseguito quando la computazione sarà completata. Questo permette di non dover attendere un risultato, ma il codice specificato, sarà eseguito al termine del calcolo delle statistiche (e.g una stampa a video del risultato).
Invece, per quanto riguarda analyzeSources(Directory directory)
, verrà restituito un oggetto osservabile, che permette di registrare, ai due componenti attivi (GUI e SourceAnalyzer), degli Handler agli eventi.
- GUI:
- emette evento di abort
- si sottoscrivere per eventi di update e complete
- SourceAnalyzer:
- emette evento di update e complete
- si sottoscrive per eventi di abort
L'approccio mediante Executor è stato implementato mediante l'utilizzo di un ForkJoinPool. Per la divisione del lavoro, sono stati individuati i seguenti Recursive Task:
- DirectoryAnalyzerTask:
- Partendo da un Path di directory, si analizza il contenuto.
- Per ogni elemento:
- se è un path di Directory, si esegue una fork di un nuovo DirectoryAnalyzerTask,
- se è un path di File, si esegue una fork di un nuovo SourceFileAnalyzerTask.
- Si esegue join su ogni fork.
- Si aggregano i risultati.
- SourceFileAnalyzerTask:
- Partendo da un Path di file, si legge il contenuto.
- Si crea il report del file.
Questo approccio facilita la versione CLI, permettendo di esplorare ricorsivamente, con gli opportuni RecursiveTask
, aggregando successivamente i risultati parziali.
Solo alla fine viene viene ritornato il risultato finale.
Invece, per la versione GUI, è stato necessario utilizzare Monitor, in modo da poter notificare e stampare a video le statistiche incrementate gradualmente.
L'approccio mediante l'utilizzo di Virtual Threads riutilizza l'idea con cui è stata realizzata l'implementazione Executor.
La differenza principale è l'utilizzo di un newVirtualThreadPerTaskExecutor
, i cui task sottomessi sono delle Callable
.
Anche in questo sono individuate i due task principali:
- VTDirectoryTask:
- Partendo da un Path di directory, si analizza il contenuto.
- Per ogni elemento:
- se è un path di Directory, si sottomette una Callable VTDirectoryTask,
- se è un path di File, si sottomette una Callable VTSourceFileTask.
- Si esegue una join sulle Callable e si aggregano i risultati.
- VTSourceFileTask:
- Partendo da un Path di file, si legge il contenuto.
- Si crea il report del file.
Come per l'approccio precedente, anche in questo caso l'implementazione favorisce la versione CLI, permettendo di esplorare ricorsivamente, con gli opportuni Callable
, aggregando successivamente i risultati parziali e minimizzando le corse critiche.
Invece, per la versione GUI, viene utilizzato il Monitor, come descritto in precedenza.
L'approccio a Event Loop è stato implementato utilizzando la libreria Vertx. L'architettura realizzata prevede un singolo Verticle che si occupa di eseguire l'esplorazione ricorsiva delle directory e calcolare il report dell'analisi. Questo approccio permette di evitare corse critiche, riducendo il codice necessario per la sincronizzazione.
Il flow dell'esecuzione è il seguente:
- Partendo da un Path, viene eseguita una chiamata asincrona per richiedere le sue props.
- Dalle props si può ricavare l'informazione sul Path:
- se è una directory, si esegue una chiamata asincrona per leggerne il contenuto,
- se è un file, si esegue una chiamata asincrona per leggere il contenuto del file.
- Al completarsi delle chiamate asincrone:
- per ogni elemento della directory, si riparte dal punto 1,
- per il contenuto del file, si crea il report e lo si aggrega al risultato.
Il problema principale di questo approccio è quello di capire quando la computazione è terminata, dal momento che le esecuzioni delle computazioni sono asincrone. L'implementazione realizzata prevede di mantenere un contatore che viene incrementato ad ogni chiamata asincrona e decrementato al completamento. In questo modo l'ultima chiamata asincrona decrementerà porta il contatore a 0, permettendo di notificare il completamento della computazione.
Per quanto riguarda l'implementazione della versione GUI viene riutilizzato quasi completamente l'approccio CLI, aggiungendo una comunicazione attraverso l'EventBus
di Vertx.
In questo modo, per terminare la computazione tramite l'interfaccia, è sufficiente inviare un messaggio sul Bus, che verrà ricevuto dal Verticle.
L'approccio Reactive è stato implementato utilizzando la libreria RxJava.
La parte principale di questo approccio è la creazione di un Observable
, che permette di esplorare la directory di partenza.
Ad esso sono aggiunte le opportune operazioni di Map e Filter, per ottenere il contenuto dei File e poterli aggregare.
Si è creato un Observable
, invece che un Flowable
, poiché lo stream dei files è lazy, così non è necessario gestire il meccanismo di Backpressure.
Le differenze tra CLI e GUI sono minime. Nel secondo caso è necessario notificare l'Observer per poter stampare a video i progressi intermedi.
Condizioni di testing:
- CPU: Intel Core i7-8700 @ 3.20GHz, 6 Core, 12 Thread
- Folder: Repo JDK
- Parametri:
- maxLines: 1000
- intervalli: 50
- longestFiles: 250
- Numero di esecuzioni: 10
La soluzione più performante in termini di tempo è quella basata su Executor, seguita subito da quella basata su Virtual Threads.
Il risultato che l'implementazione a Event Loop è la più lenta era atteso, poiché la computazione è svolta da un singolo thread.
N. | Executor (ms) | VirtualThread (ms) | EventLoop (ms) | Reactive (ms) |
---|---|---|---|---|
1 | 4077 | 5785 | 52132 | 18412 |
2 | 3354 | 3414 | 52171 | 16965 |
3 | 3114 | 3072 | 50270 | 17005 |
4 | 3044 | 3105 | 51961 | 16907 |
5 | 3210 | 3159 | 51622 | 17104 |
6 | 3130 | 3070 | 52242 | 18175 |
7 | 3036 | 3141 | 51835 | 18094 |
8 | 2901 | 3088 | 50788 | 17191 |
9 | 2975 | 3235 | 51232 | 16776 |
10 | 3141 | 3136 | 50780 | 16813 |
mean | 3198,2 | 3420,5 | 51503,3 | 17344 |
Anche nelle implementazioni per la GUI vediamo un andamento delle performance paragonabile, notando un incremento generale dei tempi di esecuzione.
N. | Executor (ms) | VirtualThread (ms) | EventLoop (ms) | Reactive (ms) |
---|---|---|---|---|
1 | 5081 | 7155 | 55118 | 21740 |
2 | 4684 | 5415 | 52816 | 21099 |
3 | 4640 | 4941 | 53943 | 21213 |
4 | 4888 | 4946 | 53893 | 20279 |
5 | 4717 | 4713 | 52778 | 20931 |
6 | 4482 | 4713 | 52957 | 20284 |
7 | 4441 | 4885 | 52737 | 22182 |
8 | 4670 | 4628 | 52047 | 20236 |
9 | 4546 | 4740 | 54733 | 20310 |
10 | 4436 | 4859 | 52925 | 20783 |
mean | 4658,5 | 5099,5 | 53394,7 | 20905,7 |