PCD Assignment 02

Componenti:

Introduzione

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

ObservableReport

Executor

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:
    1. Partendo da un Path di directory, si analizza il contenuto.
    2. 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.
    3. Si esegue join su ogni fork.
    4. Si aggregano i risultati.
  • SourceFileAnalyzerTask:
    1. Partendo da un Path di file, si legge il contenuto.
    2. Si crea il report del file.

TaskExecutor

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.

Virtual Threads

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:
    1. Partendo da un Path di directory, si analizza il contenuto.
    2. Per ogni elemento:
      • se è un path di Directory, si sottomette una Callable VTDirectoryTask,
      • se è un path di File, si sottomette una Callable VTSourceFileTask.
    3. Si esegue una join sulle Callable e si aggregano i risultati.
  • VTSourceFileTask:
    1. Partendo da un Path di file, si legge il contenuto.
    2. 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.

Event Loop

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:

  1. Partendo da un Path, viene eseguita una chiamata asincrona per richiedere le sue props.
  2. 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.
  3. 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.

EventLoop

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.

Reactive

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.

Performance

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

CLI

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

Performance CLI

GUI

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

Performance GUI