Parte II — Programmazione asincrona e reattiva · Capitolo 13

Programmazione reattiva

~20 min di letturaesempi in RxJava 3

In questo capitolo

  1. Dall'assegnamento alla relazione
  2. Event stream e behaviour
  3. Come si propaga il cambiamento: push, pull, glitch
  4. Lifting e composizione
  5. Reactive Extensions: l'Observer fatto bene
  6. RxJava: creare, trasformare, osservare
  7. Su quali thread: gli scheduler
  8. Hot e cold
  9. Backpressure
  10. Verifica le tue conoscenze

Il capitolo in un paragrafo. Callback e promise gestiscono un risultato che arriva una volta sola. Molte applicazioni però devono gestire flussi di dati e di eventi asincroni. La programmazione reattiva li tratta come valori che variano nel tempo e che si possono comporre. Il programmatore dichiara le relazioni tra i valori, e il linguaggio o il framework propaga i cambiamenti, senza callback e senza stati inconsistenti (glitch). Le Reactive Extensions portano l'idea nei linguaggi di tutti i giorni: flussi osservabili, operatori per comporli, scheduler per scegliere i thread e backpressure quando il consumatore è più lento del produttore.

1. Dall'assegnamento alla relazione

La programmazione a eventi ha problemi che le promise e async/await non eliminano. In un'applicazione asincrona l'ordine di arrivo degli eventi esterni non si può prevedere né controllare, e il controllo salta da un handler all'altro. È l'inversione del controllo: il flusso lo guidano gli eventi, non un ordine scelto dal programmatore. Quando un dato cambia, il programmatore deve aggiornare a mano tutto ciò che ne dipende, ed è facile farlo nel momento sbagliato o nell'ordine sbagliato. Tanti frammenti di codice isolati manipolano gli stessi dati in un ordine imprevedibile (lo spaghetti asincrono). E poiché le callback di solito non restituiscono nulla, possono agire solo con effetti collaterali sullo stato.

La programmazione reattiva (RP) è un paradigma orientato ai flussi di dati e alla propagazione del cambiamento. Il programma resta una reazione a eventi esterni, ma è il linguaggio a gestire lo scorrere del tempo e le dipendenze tra dati e calcoli. Il programmatore scrive che cosa fare, e il sistema decide quando: si astrae dalla gestione del tempo come il garbage collector astrae dalla gestione della memoria. Nell'esempio delle slide

var1 = 1
var2 = 2
var3 = var1 + var2

var3 dipende da var1 e var2: ogni volta che una delle due cambia, var3 viene ricalcolata automaticamente.

Per l'esame — domanda chiesta

Che differenza c'è tra una variabile imperativa e una reattiva? Dopo x = 1; y = x + 1; x = 2, in un linguaggio imperativo y vale 2. L'assegnamento dà un nome al valore di x + 1 in quel momento, e i cambiamenti successivi di x non lo toccano. In un linguaggio reattivo y vale 3. L'assegnamento dichiara una relazione, come l'uguale della matematica: y deve valere x + 1 in ogni istante. Allo stesso modo, a = b + c viene rivalutata ogni volta che cambia b o c.

La variabile reattiva è un valore che varia nel tempo, un flusso, e il sistema mantiene il grafo delle dipendenze e propaga i cambiamenti. Senza RP lo si farebbe a eventi: x come sorgente di eventi, un handler che a ogni cambiamento ricalcola y. Ma allora aggiornamenti concorrenti e consistenza (sezione 3) sarebbero un problema del programmatore.

Le radici sono nella ricerca. La maggior parte del lavoro sulla RP discende da Fran (Elliott e Hudak, 1997), un linguaggio funzionale per la grafica e le animazioni interattive. Da lì viene la Functional Reactive Programming (FRP), basata sul paradigma synchronous dataflow (Lee e Messerschmitt, 1987) con vincoli real-time più rilassati. Il riferimento del corso è il survey di Bainomugisha e colleghi (ACM Computing Surveys, 2013). Nata negli anni '90, oggi la RP si usa per le app web reattive e per i big data.

Dalle slide: linguaggi e framework reattivi

FrTime (basato su Scheme) · le Reactive Extensions (Rx) di Microsoft · per JavaScript Flapjax, Bacon.js e AngularJS · Reactive Dart · Scala.React · RxJava.

2. Event stream e behaviour

La RP rappresenta come astrazioni i valori che variano nel tempo. Sono valori di prima classe: si passano ad altre funzioni e si compongono. Le astrazioni sono di due tipi:

Un esempio in Flapjax, una libreria reattiva per JavaScript: un elemento della pagina che mostra i secondi trascorsi.

var timer   = timerB(100);                          // behaviour: il tempo, aggiornato ogni 100 ms
var seconds = liftB(function (time) {               // behaviour derivato da timer
                return Math.floor(time / 1000);
              }, timer);
insertDomB(seconds, 'timer-div');                   // l'elemento della pagina segue seconds

Non ci sono callback né listener. seconds si ottiene da timer come map ottiene una lista da un'altra: per ogni valore di timer c'è il valore corrispondente di seconds. La pagina si aggiorna da sola. Tutto è dichiarativo.

3. Come si propaga il cambiamento: push, pull, glitch

Il modello di valutazione di un linguaggio reattivo stabilisce come i cambiamenti si propagano lungo il grafo delle dipendenze. Per il programmatore la propagazione è automatica. Chi progetta il linguaggio deve però decidere chi la avvia.

In entrambi i casi i dati vanno dal produttore al consumatore: cambia chi prende l'iniziativa.

I glitch

Un glitch è un'inconsistenza temporanea durante la propagazione. Un calcolo viene eseguito prima che siano state valutate tutte le espressioni da cui dipende, e combina valori nuovi con valori vecchi (Cooper e Krishnamurthi, 2006). Riguarda solo il modello push. L'esempio delle slide:

var1 = 1
var2 = var1 * 1        // deve valere sempre quanto var1
var3 = var1 + var2     // deve valere sempre il doppio di var1

Rendere osservabile lo stato di t₂ vuol dire mostrare al mondo esterno uno stato che, secondo le relazioni dichiarate, non è mai esistito. Per evitarlo (glitch avoidance) le librerie costruiscono il grafo delle dipendenze e aggiornano un nodo solo dopo tutti quelli da cui dipende, in ordine topologico. Gli aggiornamenti diventano così atomici rispetto all'osservatore. Le implementazioni recenti evitano i glitch su un solo computer. Nei programmi reattivi distribuiti è ancora un problema aperto, per via di guasti, ritardi di rete e assenza di un orologio globale (Salvaneschi et al. 2014, Mogk et al. 2018): la RP distribuita, insieme alla tolleranza ai guasti, è una delle direzioni di ricerca attuali.

4. Lifting e composizione

Un valore che dipende da valori reattivi deve essere reattivo a sua volta: una variabile a cui si assegna un'espressione con behaviour o event stream diventa reattiva. Il lifting è la conversione di una variabile, o di una funzione, normale in una reattiva. liftB nell'esempio del timer applica la funzione ordinaria time => Math.floor(time / 1000) a un behaviour. È il lifting a registrare le dipendenze: per ricalcolare correttamente tutte le espressioni, la libreria costruisce dietro le quinte il grafo delle dipendenze. Quando timer cambia, il grafo sa che va rivalutata la funzione, poi aggiornato seconds e poi la pagina.

Il lifting può essere di tre tipi:

La composizione di astrazioni reattive è ciò che evita l'incubo delle callback. Tre callback separate per click, movimento e rilascio del mouse diventano un solo event stream che risponde a tutti e tre. Bacon.js compone i behaviour con combine, Flapjax gli event stream con mergeE:

var saveTimer   = timerE(10000);                          // un evento ogni 10 secondi
var saveClicked = extractEventE('save-button', 'click');  // un evento a ogni click su Save
var save        = mergeE(saveTimer, saveClicked);         // l'uno o l'altro
save.mapE(doSave);                                        // salva a ogni evento di save

«Salva ogni 10 secondi o quando l'utente preme Save» diventa quattro righe dichiarative, senza gestire né il tempo né il pulsante.

5. Reactive Extensions: l'Observer fatto bene

Nei linguaggi mainstream la RP arriva come libreria: le Reactive Extensions (Rx), un'API per la programmazione asincrona con flussi osservabili; Reactive Streams, uno standard per flussi asincroni con backpressure non bloccante (JVM e JavaScript); Project Reactor, una libreria per la JVM basata su Reactive Streams. Rx nasce su .NET dalla ricerca sulla FRP e oggi esiste per quasi ogni linguaggio (RxJS, RxJava, RxScala, RxPY…), con la stessa semantica; Angular la usa nel frontend e nel backend. Si presenta come «l'Observer fatto bene», una combinazione delle idee migliori del pattern Observer, del pattern Iterator e della programmazione funzionale (E. Meijer, Your mouse is a database, 2012).

Nel pattern Observer un oggetto osservabile (il subject) tiene una lista di osservatori e li notifica con un push quando accade qualcosa, senza conoscere chi li usa. Rx estende l'idea: una sorgente emette una sequenza di valori e ne segnala la fine o l'errore, le sequenze si compongono con operatori come le collezioni, e una sottoscrizione si può annullare.

interface IObservable<T> { IDisposable Subscribe(IObserver<T> observer); }
interface IObserver<T>   { void OnNext(T value); void OnError(Exception error); void OnCompleted(); }
interface IDisposable    { void Dispose(); }      // annulla la sottoscrizione

Le proprietà fondamentali di Rx sono tre:

Per le slide, in .NET la Subscribe è asincrona: il chiamante non resta bloccato fino alla fine della sequenza, che può girare su un altro thread. Rx si integra con i flussi sincroni (IEnumerable<T>) e con i calcoli asincroni a valore singolo (Task<T>), e gli operatori LINQ trasformano (aggregazione, proiezione), compongono (Zip, SelectMany) e interrogano (Where, Any). Concat di due Range(1, 3) dà 1, 2, 3, 1, 2, 3, perché la seconda parte solo quando la prima è finita; Merge le rende attive insieme, e l'esempio delle slide dà 1, 1, 2, 2, 3, 3.

Oltre le slide

L'interleaving di Merge dipende dallo scheduler delle sorgenti. In RxJava Observable.range emette in modo sincrono durante la sottoscrizione, quindi range(1, 3).mergeWith(range(1, 3)) stampa 1, 2, 3, 1, 2, 3.

Quattro idee comuni

La documentazione di Project Reactor riassume ciò che hanno in comune queste librerie:

6. RxJava: creare, trasformare, osservare

RxJava è Rx per la JVM: estende il pattern Observer alle sequenze di dati ed eventi e aggiunge operatori per comporle, nascondendo thread, sincronizzazione, strutture dati concorrenti e I/O non bloccante. La 1.x era Rx sopra Java, la 2.x (2017) si è allineata a Reactive Streams, e le slide indicano la 4.x (2026) come ultima major release; gli esempi usano l'API di RxJava 3. I flussi sono Observable, leggero e senza backpressure, e Flowable, più costoso, per grandi quantità di dati e con backpressure; per il resto sono equivalenti.

Flowable.range(1, 20)                          // creazione: i numeri da 1 a 20
    .map(v -> v * v)                           // trasformazione: i quadrati
    .filter(v -> v % 3 == 0)                   // filtro: solo i multipli di 3
    .subscribe(System.out::println);           // 9, 36, 81, 144, 225, 324

Observable<Integer> source = Observable.create(emitter -> {   // un flusso scritto a mano
    for (int i = 0; i < 3; i++) emitter.onNext(i);
    emitter.onComplete();                      // chiude il flusso
});
Disposable d = source.subscribe(
    v -> log("ricevuto " + v),                 // onNext
    err -> log("errore " + err),               // onError: gli operatori propagano, gestisce il subscriber
    () -> log("completato"));                  // onComplete
// d.dispose() annulla la sottoscrizione (in RxJava 1 era subscription.unsubscribe())

Ogni operatore crea un flusso nuovo: nessun flusso viene modificato e non ci sono effetti collaterali, il che rende questi sistemi efficienti anche in concorrenza. La catena non contiene elementi, sa come generarli. Senza scheduler la subscribe è sincrona: il thread chiamante (qui il main) genera gli elementi ed esegue le lambda. Le eccezioni di un operatore non si gestiscono lì: arrivano all'onError del subscriber.

Per creare flussi: just, fromArray, fromIterable e range fanno lifting di dati esistenti; create riceve una lambda che chiama onNext sull'emitter; fromCallable dà il risultato di un calcolo; interval emette un intero a ogni periodo. Un flusso generalizza la programmazione asincrona a più risultati nel tempo, dove una promise ne ha uno solo. flatMap trasforma ogni elemento in un flusso e li fonde in uno solo. zipWith accoppia gli elementi di due flussi a due a due, qualunque sia il momento in cui arrivano:

Flowable.just("the", "quick", "brown", "fox")
    .zipWith(Flowable.range(1, 5), (w, n) -> n + ". " + w)
    .subscribe(System.out::println);           // 1. the, 2. quick, 3. brown, 4. fox

Ogni operatore è documentato da un marble diagram: flusso d'ingresso su una linea del tempo, operatore, flusso d'uscita. doOnNext lascia passare gli elementi eseguendo un'azione, utile per il debug; operatori temporali come debounce, sample e buffer esprimono dichiarativamente comportamenti legati al tempo (sezione 8).

Dalle slide: le famiglie di operatori (liste complete)

Famiglie: creazione · trasformazione · filtro · combinazione · condizionali e booleani · matematici e di aggregazione · backpressure.

Trasformazione: Map applica una funzione a ogni elemento · Scan applica una funzione cumulativa ed emette ogni valore intermedio · FlatMap trasforma gli elementi in flussi e ne fonde le emissioni · Buffer raccoglie periodicamente gli elementi in pacchetti · GroupBy divide il flusso in più flussi per chiave · Window suddivide periodicamente il flusso in finestre, che sono flussi.

Filtro: Filter (predicato) · Skip (primi n) · Take (solo i primi n) · Debounce (emette un elemento solo se è passato un intervallo senza altre emissioni) · Distinct (niente duplicati) · ElementAt (l'n-esimo) · First (il primo, o il primo che soddisfa una condizione) · IgnoreElements (nessun elemento, solo la terminazione) · Last · Sample (l'elemento più recente a intervalli regolari) · SkipLast · TakeLast.

Combinazione: Merge (fonde le emissioni) · Zip (combina gli elementi a due a due con una funzione) · And/Then/When (combina insiemi di elementi tramite Pattern e Plan) · Join (combina un elemento di un flusso con quelli dell'altro emessi in una finestra temporale) · CombineLatest (a ogni emissione combina l'ultimo elemento di ciascun flusso) · StartWith (premette una sequenza) · Switch (da un flusso di flussi, segue solo il più recente).

7. Su quali thread: gli scheduler

In RxJava non si creano thread: si dichiara su quale scheduler, un'astrazione simile a un executor, deve girare una parte della catena.

Gli scheduler predefiniti sono tre. Schedulers.computation() è per il lavoro CPU-bound, su un numero fisso di thread dedicati, ed è il default della maggior parte degli operatori asincroni. Schedulers.io() è per l'I/O e le operazioni bloccanti, su un insieme di thread che cresce e cala. Schedulers.single() esegue tutto su un solo thread, in ordine FIFO. Con Schedulers.from(executor) si avvolge un executor esistente, ad esempio per avere un pool fisso più grande.

Flowable.fromCallable(() -> { Thread.sleep(1000); return "Done"; })   // calcolo costoso o I/O bloccante
    .subscribeOn(Schedulers.io())                  // …su un thread di I/O
    .observeOn(Schedulers.single())                // il risultato si elabora su un altro thread (es. la GUI)
    .subscribe(System.out::println, Throwable::printStackTrace);

Flowable.range(1, 10)
    .flatMap(v -> Flowable.just(v)
        .subscribeOn(Schedulers.computation())     // ogni sottoflusso su un thread del pool
        .map(w -> w * w))
    .blockingSubscribe(v -> log("sub " + v));      // osservati dal thread chiamante, uno alla volta

Il secondo esempio è lo schema del parallelismo. subscribeOn su una catena sola la sposta su un altro thread, ma non ne divide gli stadi. Dentro flatMap, invece, ogni sottoflusso ha il suo subscribeOn, e i quadrati sono calcolati in parallelo su thread diversi del pool, arrivando fuori ordine. blockingSubscribe riporta l'osservazione su un solo thread, quello che la chiama. Così gli elementi calcolati in parallelo si elaborano in sequenza, senza lock. Conviene prima far funzionare la catena senza scheduler e solo dopo inserire subscribeOn e observeOn dove serve parallelizzare: la logica non cambia.

Oltre le slide

Se una catena contiene più subscribeOn, vale quello più vicino alla sorgente.

8. Hot e cold

La famiglia Rx distingue due categorie di flussi, secondo come reagiscono ai subscriber.

ConnectableObservable<Long> hot = Observable.interval(10, TimeUnit.MILLISECONDS).publish();
hot.connect();                                     // parte subito, anche senza subscriber
Thread.sleep(100);
hot.subscribe(v -> log("A " + v));                 // vede solo gli elementi da qui in poi

In questa forma di base, il thread che genera gli elementi di un flusso hot è anche quello che esegue gli osservatori: un osservatore lento rallenta la sorgente. Per disaccoppiarli si usa observeOn.

PublishSubject: un ponte con il codice non reattivo

Nei flussi creati con create gli elementi li genera la lambda passata alla creazione. Un PublishSubject invece è un flusso hot che fa anche da canale: chiunque, da qualsiasi thread, può chiamare onNext per inserirvi elementi (non da più thread contemporaneamente: un subject non è thread-safe, e per questo esiste toSerialized()), e chiunque può sottoscriversi. È il modo più semplice di collegare codice legacy o una GUI al mondo reattivo. Va però usato con giudizio, perché permette di scrivere codice poco leggibile. Un esempio classico: rilevare i «multi-click», cioè almeno due click separati da meno di 250 ms.

PublishSubject<Integer> clicks = PublishSubject.create();
button.addActionListener(e -> clicks.onNext(1));          // Swing produce elementi nel flusso

clicks.buffer(clicks.debounce(250, TimeUnit.MILLISECONDS))  // raggruppa i click fino a 250 ms di silenzio
      .map(List::size)
      .filter(n -> n >= 2)
      .subscribe(n -> System.out.println("multi-click: " + n));

debounce emette un elemento solo dopo 250 ms senza altri click, e segna così la fine di ogni raffica. buffer usa quei segnali come confini per raggruppare i click. Il resto è map e filter. Senza operatori, la stessa logica richiederebbe timer, stato e callback.

9. Backpressure

Ogni stadio di una catena consuma ciò che produce il precedente, e gli stadi possono avere velocità molto diverse. In un produttore-consumatore classico il buffer pieno blocca il produttore; qui bloccare non si può, e gli elementi in eccesso si accumulano in buffer sempre più grandi o vanno scartati. La backpressure è un controllo di flusso in cui ogni stadio dichiara quanti elementi è pronto a elaborare: il segnale che una stazione lenta della catena di montaggio manda a monte per limitare il flusso.

Un subscriber può lavorare in modalità unbounded, lasciando che la sorgente spinga i dati alla massima velocità. Oppure può usare il meccanismo di request: chiede al massimo n elementi. Il modello push diventa un ibrido push-pull. Il consumatore tira n elementi se sono già disponibili; se non lo sono, la sorgente li spinge appena li produce. Nel caso cold il problema quasi non si pone, perché è il subscriber a tirare gli elementi. Nel caso hot è il generatore a spingerli. In RxJava la backpressure c'è solo per Flowable, ed è obbligatoria nello standard Reactive Streams.

Flowable.interval(5, TimeUnit.MILLISECONDS)                     // un elemento ogni 5 ms
    .observeOn(Schedulers.single())
    .subscribe(v -> { Thread.sleep(100); log("consumo " + v); },  // ne consuma uno ogni 100 ms
               err -> log("errore: " + err));                     // MissingBackpressureException

Dopo poco il buffer interno si riempie e RxJava segnala un errore: non può emettere un valore perché nessuno lo ha richiesto. Le strategie per evitarlo sono di due tipi. Si può ridurre il flusso con operatori: buffer(n) raggruppa gli elementi in pacchetti, window(n) li raggruppa in lotti che sono flussi, e sample(t), debounce(t) e throttleFirst(t) ne saltano una parte. Oppure si può dire che cosa fare quando il buffer si riempie:

source.onBackpressureBuffer(5000,                                // un buffer più grande…
        () -> log("buffer pieno"),                              // …con una reazione al riempimento
        BackpressureOverflowStrategy.DROP_OLDEST);              // ERROR, DROP_OLDEST o DROP_LATEST
source.onBackpressureDrop();                                     // scarta tutto ciò che è in eccesso
Flowable.create(emitter -> { /* onNext… */ }, BackpressureStrategy.BUFFER);   // o la strategia scelta alla creazione
Oltre le slide

Nelle lab notes le strategie di overflow compaiono con i nomi di RxJava 1 (ON_OVERFLOW_ERROR, ON_OVERFLOW_DROP_LATEST, ON_OVERFLOW_DROP_OLDEST); in RxJava 3 sono le costanti di BackpressureOverflowStrategy qui sopra. Cold non significa sempre «si può tirare»: interval è cold (ogni subscriber ha il suo timer) ma produce a tempo, e con un consumatore lento ha bisogno di backpressure come un flusso hot.

Nessuna pallottola d'argento

Né la RP né Rx sono il modello generale per la programmazione asincrona. Sono efficaci per gestire flussi di dati e di eventi in stile funzionale, e il loro vantaggio si vede solo se la logica del problema si esprime come composizione di flussi. Usare Rx solo per sottoscrivere lambda, come un Observer qualsiasi, non è programmazione reattiva. Resta aperta la sfida di integrare gli approcci: sincrono e asincrono, push e pull, concorrenza con più event loop che comunicano (cioè gli attori), e modelli che funzionino anche nei sistemi distribuiti. Le domande su come l'assignment 2 usa la programmazione reattiva e le promise sono nella pagina di preparazione.

Verifica le tue conoscenze

Che cos'è la programmazione reattiva?

Un paradigma orientato ai flussi di dati e alla propagazione del cambiamento. Si dichiarano le relazioni tra valori che variano nel tempo, e il linguaggio o la libreria aggiorna automaticamente tutto ciò che dipende da un valore quando questo cambia.

Quali sono le due astrazioni reattive di base?

Gli event stream, valori discreti nel tempo come i click, e i behaviour o signal, valori continui con un valore in ogni istante, come un timer o una temperatura.

Che differenza c'è tra valutazione push e pull?

In pull chi ha bisogno di un valore lo chiede alla sorgente (demand-driven, lazy). In push la sorgente, quando ha un dato nuovo, lo manda a chi ne dipende (data-driven, eager).

Che cos'è un glitch e come si evita?

Un'inconsistenza temporanea della propagazione push: un valore viene ricalcolato combinando dati nuovi e vecchi, come var3 = 3 invece di 4. Si evita aggiornando i nodi del grafo delle dipendenze in ordine topologico; nei sistemi distribuiti è un problema aperto.

Che cos'è il lifting?

La conversione di una variabile o funzione normale in una reattiva, che registra la dipendenza nel grafo. Può essere implicito (Bacon.js), esplicito (React.js) o entrambi (Flapjax).

Che cosa succede prima e dopo la subscribe?

Prima si costruisce solo la descrizione della catena di operatori, e nessun dato scorre. La subscribe collega il subscriber, e la sua richiesta risale fino alla sorgente e fa partire il flusso. Un flusso hot è l'eccezione, perché emette comunque.

Che differenza c'è tra subscribeOn e observeOn?

subscribeOn sceglie lo scheduler dell'intera catena, dalla sorgente, ovunque sia scritto. observeOn cambia scheduler solo per gli stadi che lo seguono.

Che differenza c'è tra un flusso hot e uno cold?

Un cold riparte da capo per ogni subscriber, che riceve tutti gli elementi, ed emette solo se qualcuno è sottoscritto. Un hot emette al proprio ritmo anche senza subscriber, e chi arriva tardi riceve solo gli elementi successivi.

Che cos'è la backpressure?

Un controllo di flusso in cui il consumatore dice al produttore quanti elementi è pronto a ricevere (request(n)), trasformando il push in un ibrido push-pull. In RxJava c'è solo su Flowable, con strategie di buffer, scarto e campionamento.

Da quali lezioni viene questo capitolo

Slide [module-2.2] Reactive Programming e [Lab Notes] Reactive Programming in Java — RxJava · 10 aprile 2026 (§11–17: dall'assegnamento alla relazione, propagazione, behaviour ed event stream, push e pull, glitch, lifting, Flapjax, operatori) · 13 aprile (§1–14, 16–17: RxJava in laboratorio: creazione, operatori, cold e hot, PublishSubject e GUI, scheduler, flatMap e parallelismo) · 20 aprile (§14–15: hot e cold, operatori temporali, backpressure). La mappa completa è in coverage-matrix.json.