Cosa succede quando un gruppo di fotografi amatoriali, trasformatisi in sviluppatori amatoriali, a cui si aggiunge una manciata di sviluppatori back-end che sviluppano librerie per sviluppatori, decide di lavorare senza metodo né struttura su un software professionale destinato agli utenti finali, la cui competenza fondamentale (colorimetria e psicofisica) si colloca da qualche parte tra una laurea triennale in fotografia e una laurea magistrale in scienze applicate, promettendo al contempo di rilasciare 2 versioni all’anno senza gestione di progetto? Il tutto, ovviamente, in un progetto dove i fondatori e la prima generazione di sviluppatori sono passati ad altro e se la sono data a gambe?

Indovina!

Degradare le funzioni di base

Gli anni 2020 arrivano con 40 anni di ritardo per reinventare i paradigmi di interazione tra utente e computer, che si tratti di come usiamo tastiera e mouse per pilotare l’interfaccia o del comportamento di un gestore di file. Fin dagli anni 1980, tutti i dispositivi informatici per il grande pubblico sono convergiti verso una semantica più o meno unificata, dove il tasto escape chiude l’applicazione corrente, il doppio clic apre i file e la rotella del mouse fa scorrere la vista corrente. Darktable1 si prende un piacere fuori luogo nell’ignorare tutto ciò e le modifiche recenti peggiorano le cose: ora è obbligatorio leggere la documentazione per svolgere compiti semplici come ordinare i file o assegnare scorciatoie da tastiera alle azioni della GUI.

Gruppi di moduli

Tutto comincia con la revisione dei gruppi di moduli , nel 2020, che nasconde la decisione di non decidere  un ordine unificato dei moduli.

image

Dal 2018 , mi batto per ripulire l’interfaccia grafica di Darktable, e in particolare l’organizzazione dei moduli. Un’interfaccia grafica dovrebbe promuovere le buone pratiche disponendo gli strumenti nell’ordine tipico in cui dovrebbero essere usati. Le cattive pratiche sono quelle che aumentano il rischio di incoerenze colorimetriche o di editing circolare, dove si deve tornare indietro per ripercuotere modifiche fatte in seguito, anche se le cattive pratiche possono funzionare nei casi semplici. Nel contesto dell’elaborazione delle immagini, un compito altamente tecnico in cui molte cose sono nascoste agli utenti finali sotto la GUI, le buone pratiche permettono anche a persone poco qualificate di usare il software in modo da ridurre la probabilità di errori.

Questo ordine d’uso è dettato per lo più da considerazioni tecniche come l’ordine di applicazione dei moduli nella sequenza della pipeline e l’uso di maschere disegnate e parametriche, il cui effetto dipende dai moduli a monte. Ignorare queste considerazioni equivale a cercare guai, anche se la moda del momento negli anni 2010 e 2020 è credere che le tecnologie digitali funzionino svincolate da qualsiasi realtà materiale, per la sola gioia dell’utente.

Per esempio, i vincoli imposti alla progettazione della pipeline per consentire un ordine arbitrario d’uso dei moduli creano problemi matematicamente irrisolvibili  riguardo al calcolo delle coordinate dei nodi delle maschere, e nessuna soluzione programmata è possibile (a parte allentare quel vincolo) perché la matematica ha detto no.

Solo che una parte significativa degli utenti-programmatori che ruotano attorno al progetto su Github e sulla mailing-list di sviluppo resta convinta che non esista un flusso di lavoro buono o cattivo, ma solo preferenze personali, il che è probabilmente vero quando pratichi un’arte senza vincoli di tempo, di budget o di risultato. In quanto tali, i moduli dovrebbero poter essere riordinati a piacimento, sia nella pipeline che nel flusso di lavoro. La confusione nasce dal fatto che l’editing non distruttivo è erroneamente visto come asincrono (il che sarebbe quasi vero se non usassimo né maschere né modalità di fusione), mentre la pipeline dei pixel è sequenziale e più vicina a una logica a livelli, come la troviamo in Adobe Photoshop, Gimp, Krita, ecc.

Disaccoppiare l’ordine dei moduli nella GUI dall’ordine della pipeline equivale ad abilitare ogni uso, anche patologico, e costringe a scrivere pagine e pagine  di documentazione per avvertire, spiegare cosa fare, come e perché; documentazione che nessuno leggerà, finendo per porre in continuazione le stesse domande ogni settimana su ogni forum.

In questa storia, tutti perdono tempo a causa di un design dell’interfaccia che cerca di essere così flessibile da non poter essere reso sicuro e robusto per impostazione predefinita. Nel corpo umano, ogni articolazione ha alcuni gradi di libertà lungo determinati assi; se ogni articolazione potesse ruotare di 340° attorno a ciascun asse dello spazio 3D, la struttura sarebbe instabile perché troppo flessibile, e incapace di lavorare con carichi elevati. La metafora regge nel software professionale. Nuotiamo nel culto del cargo  del FLOSS, dove alla gente piace avere l’illusione della scelta, cioè che vengano offerte molte opzioni di cui la maggior parte sono inutilizzabili o pericolose, a scapito della semplicità (KISS ), e dove la maggioranza degli utenti non comprende le implicazioni di ciascuna opzione (e non ha il minimo desiderio di comprenderle).

In assenza di un consenso sull’ordinamento dei moduli nell’interfaccia, alla fine del 2020 è stato introdotto uno strumento complicato, fragile e pesante per consentire a ciascun utente di configurare la disposizione dei moduli in schede. Offre molte opzioni inutili e memorizza la disposizione corrente nel database, usando il nome tradotto dei moduli, il che significa che cambiare la lingua dell’interfaccia ti fa perdere i preset. Interamente configurabile, lascia che l’utente decida come farsi del male, senza alcuna guida alle buone pratiche. Questa spazzatura è codificata in 4000 righe che mescolano allegramente richieste SQL in mezzo al codice GTK dell’interfaccia , e i preset vengono creati tramite macro ridondanti del compilatore , mentre i moduli hanno da sempre un flag binario , che consente di impostare il loro gruppo predefinito… modularmente.

Cosa più importante, sostituisce una funzione semplice ed efficiente, disponibile fino a Darktable 3.2:

image

Un clic sul nome del modulo lo abilita, un secondo lo aggiunge alla colonna dei preferiti, un terzo lo nasconde dall’interfaccia. Tutto ciò consentendo i preset e memorizzando la disposizione corrente in testo semplice nel file darktablerc. Semplice e robusta, codificata in 688 righe  di codice leggibile e ben strutturato, la funzione non era dunque abbastanza divertente per lo sviluppatore dilettante di mezza età, ed era urgente sostituirla con un sistema labirintico.

Scorciatoie da tastiera

Nel 2021 è stato aggiunto quello che io chiamo il grande turducken  MIDI. L’obiettivo è estendere l’interfaccia delle scorciatoie da tastiera (già estesa nel 2019 per supportare le «scorciatoie dinamiche», che consentono di combinare azioni di mouse e tastiera), per supportare dispositivi MIDI e… controller da videogioco.

Alla fine del 2022, cioè un anno e mezzo dopo questa funzione, nel sondaggio che ho condotto , meno del 10% degli utenti possiede un dispositivo MIDI, e solo il 2% lo usa con Darktable. Da confrontare con il 45% di utenti che possiede una tavoletta grafica (tipo Wacom), il cui supporto in Darktable è ancora così carente che solo il 6% la usa. A prescindere dalla pessima gestione delle priorità, ciò che non tollero qui sono gli effetti collaterali introdotti da questa modifica e il costo globale che ha avuto, a cominciare dal fatto che non importa le scorciatoie definite dall’utente delle versioni precedenti alla 3.2, e rende terribilmente complicata la configurazione di nuove scorciatoie.

Prima del grande turducken, solo un elenco limitato di azioni della GUI poteva essere associato a scorciatoie da tastiera o miste (tastiera + mouse). Questo elenco era curato manualmente dagli sviluppatori. Il grande turducken MIDI consente di associare ogni azione della GUI a delle scorciatoie, presentando agli utenti un elenco di diverse migliaia di voci configurabili, in cui è difficile trovare le sole 3 di cui hai davvero bisogno, e il motore di ricerca testuale è troppo elementare per essere d’aiuto:

image

Nota l’uso di «effetti», sui quali la documentazione  non è di alcun aiuto. È solo per deduzione (perché nemmeno il codice è commentato ) che ho finito per capire che sono emulazioni di interazioni desktop tipiche (mouse e tastiera) destinate a essere usate con dispositivi MIDI e controller gamepad (ma devi ancora spiegarmi cosa significhino Ctrl-Toggle, Right-activate o Right-Toggle in termini di interazione desktop tipica).

Ciò che è inaccettabile è che l’uso del tastierino numerico sia rotto per progettazione , in particolare per attribuire valutazioni numerate (stelle) alle miniature nel tavolo luminoso. Infatti, i modificatori dei tasti (numlock e capslock) non vengono decodificati correttamente dall’aggeggio, e i numeri vengono trattati diversamente a seconda che siano inseriti dalla tipica tastiera «di testo» o dal tastierino numerico. Così l’1 del tastierino viene decodificato come Keypad End, indipendentemente dallo stato del numlock. È così che ho dovuto configurare le scorciatoie numeriche su una tastiera francese BÉPO e duplicare la configurazione per il tastierino numerico:

image

Devi solo ricordarti che Shift+" e Kp End significano entrambi 1 e ricordarti di duplicare tutte le scorciatoie per il tastierino numerico e per il resto della tastiera. In breve, si infrange un’aspettativa di base dell’utente, e si manda al diavolo la critica al design. La regressione è menzionata su tutti i forum di Darktable ma sembra non infastidire nessuno.

La correzione di questo bug questa funzione è stata fatta in Ansel  e i tasti del tastierino numerico vengono rimappati su tasti standard direttamente nel codice , per un totale di 100 righe di codice compresi i commenti. Fare questa correzione è stato davvero molto difficile: ho letto la documentazione di Gtk  e ho preso il loro esempio riga per riga. 2 anni passati ad aspettare quello…

La ciliegina sulla torta è che, ancora una volta, abbiamo sostituito 1306 righe di codice chiaro e strutturato  con una mostruosità di quasi 4400 righe , con gemme come:

  1. il ciclo while della morte (sorgente ):
 1  gboolean applicable;
 2  while((applicable =
 3           (c->key_device == s->key_device && c->key == s->key && c->press >= (s->press & ~DT_SHORTCUT_LONG) &&
 4           ((!c->move_device && !c->move) ||
 5             (c->move_device == s->move_device && c->move == s->move)) &&
 6           (!s->action || s->action->type != DT_ACTION_TYPE_FALLBACK ||
 7            s->action->target == c->action->target))) &&
 8        !g_sequence_iter_is_begin(*current) &&
 9        (((c->button || c->click) && (c->button != s->button || c->click != s->click)) ||
10         (c->mods       && c->mods != s->mods ) ||
11         (c->direction  & ~s->direction       ) ||
12         (c->element    && s->element         ) ||
13         (c->effect > 0 && s->effect > 0      ) ||
14         (c->instance   && s->instance        ) ||
15         (c->element    && s->effect > 0 && def &&
16          def->elements[c->element].effects != def->elements[s->element].effects ) ))
17  {
18    *current = g_sequence_iter_prev(*current);
19    c = g_sequence_get(*current);
20  }
  1. Lo switch case contenente if annidati su 2 livelli (sorgente ):
 1  switch(owner->type)
 2  {
 3  case DT_ACTION_TYPE_IOP:
 4    vws = DT_VIEW_DARKROOM;
 5    break;
 6  case DT_ACTION_TYPE_VIEW:
 7    {
 8      dt_view_t *view = (dt_view_t *)owner;
 9
10      vws = view->view(view);
11    }
12    break;
13  case DT_ACTION_TYPE_LIB:
14    {
15      dt_lib_module_t *lib = (dt_lib_module_t *)owner;
16
17      const gchar **views = lib->views(lib);
18      while(*views)
19      {
20        if     (strcmp(*views, "lighttable") == 0)
21          vws |= DT_VIEW_LIGHTTABLE;
22        else if(strcmp(*views, "darkroom") == 0)
23          vws |= DT_VIEW_DARKROOM;
24        else if(strcmp(*views, "print") == 0)
25          vws |= DT_VIEW_PRINT;
26        else if(strcmp(*views, "slideshow") == 0)
27          vws |= DT_VIEW_SLIDESHOW;
28        else if(strcmp(*views, "map") == 0)
29          vws |= DT_VIEW_MAP;
30        else if(strcmp(*views, "tethering") == 0)
31          vws |= DT_VIEW_TETHERING;
32        else if(strcmp(*views, "*") == 0)
33          vws |= DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
34                 DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
35        views++;
36      }
37    }
38    break;
39  case DT_ACTION_TYPE_BLEND:
40    vws = DT_VIEW_DARKROOM;
41    break;
42  case DT_ACTION_TYPE_CATEGORY:
43    if(owner == &darktable.control->actions_fallbacks)
44      vws = 0;
45    else if(owner == &darktable.control->actions_lua)
46      vws = DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
47            DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
48    else if(owner == &darktable.control->actions_thumb)
49    {
50      vws = DT_VIEW_DARKROOM | DT_VIEW_MAP | DT_VIEW_TETHERING | DT_VIEW_PRINT;
51      if(!strcmp(action->id,"rating") || !strcmp(action->id,"color label"))
52        vws |= DT_VIEW_LIGHTTABLE; // lighttable has copy/paste history shortcuts in separate lib
53    }
54    else
55      fprintf(stderr, "[find_views] views for category '%s' unknown\n", owner->id);
56    break;
57  case DT_ACTION_TYPE_GLOBAL:
58    vws = DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
59          DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
60    break;
61  default:
62    break;
63  }
  1. Lo switch case annidato del demonio, con clausole additive subdolamente nascoste (sorgente ):
 1case DT_ACTION_ELEMENT_ZOOM:
 2    ;
 3    switch(effect)
 4    {
 5    case DT_ACTION_EFFECT_POPUP:
 6      dt_bauhaus_show_popup(widget);
 7      break;
 8    case DT_ACTION_EFFECT_RESET:
 9      move_size = 0;
10    case DT_ACTION_EFFECT_DOWN:
11      move_size *= -1;
12    case DT_ACTION_EFFECT_UP:
13      _slider_zoom_range(bhw, move_size);
14      break;
15    case DT_ACTION_EFFECT_TOP:
16    case DT_ACTION_EFFECT_BOTTOM:
17      if((effect == DT_ACTION_EFFECT_TOP) ^ (d->factor < 0))
18        d->max = d->hard_max;
19      else
20        d->min = d->hard_min;
21      gtk_widget_queue_draw(widget);
22      break;
23    default:
24      fprintf(stderr, "[_action_process_slider] unknown shortcut effect (%d) for slider\n", effect);
25      break;
26    }

I programmatori capiscono di cosa parlo; per gli altri, sappiate soltanto che io non capisco più di voi cosa faccia tutto ciò: è codice di merda, e se diversi bug non sono nascosti lì dentro, sarà pura fortuna. Dare la caccia ai bug in questo letamaio è archeologia sul fondo delle fogne, tanto più considerando che Darktable non dispone di una documentazione per sviluppatori e, in assenza di commenti significativi nel codice, qualsiasi modifica del codice suddetto comincerà necessariamente con una fase di reverse-engineering sempre più difficile con il passare del tempo.

Il vero problema di questo genere di codice è che non lo puoi migliorare senza riscriverlo più o meno interamente: per correggerlo, devi prima capirlo, ma la ragione per cui va corretto è precisamente che non è comprensibile ed è pericoloso a lungo termine. Lo chiamiamo debito tecnico . In breve, tutto il lavoro investito su questa funzione creerà lavoro extra perché è irragionevole tenere quel genere di codice in mezzo a una base di codice di diverse centinaia di migliaia di righe e aspettarsi che non ci esploda in faccia un giorno.

È tanto più ridicolo nel contesto di un’applicazione open-source/libera dove il grosso del personale è composto da programmatori non formati. Gli sviluppatori intelligenti scrivono codice comprensibile dagli idioti, e viceversa.

Filtri delle collezioni

Fino a Darktable 3.8, i filtri delle collezioni, in cima al tavolo luminoso, venivano usati per restringere temporaneamente la vista su una collezione. La collezione è un’estrazione dal database delle foto basata su determinati criteri, il più comune dei quali è l’estrazione del contenuto di una cartella (che Darktable chiama «filmroll» per confondere tutti, perché un filmroll è in realtà il contenuto di una cartella visualizzato come elenco piatto invece che ad albero - molte persone pensano erroneamente che Darktable non abbia un gestore di file).

Essendo stato un utente di Darktable per più di un decennio, ho un database di più di 140.000 voci. Estrarre una collezione tra queste 140.00 immagini è un’operazione lenta. Ma le mie cartelle raramente contengono più di 300 immagini. Filtrare, per esempio, le immagini valutate 2 stelle o più, in una collezione di 300 file, è veloce perché è un sottoinsieme di 300 elementi. E anche passare da un filtro all’altro è veloce. Il filtro è solo una vista parziale o totale di una collezione, ottimizzata per un uso rapido e temporaneo di tipo start-and-go.

Con il pretesto di rifattorizzare il codice di filtraggio, che occupava tutto sommato 550 righe , il capo Gaston Lagaffe  si è fatto una vocazione di rompere questo modello per trasformare i filtri delle collezioni in collezioni di base, per mezzo di più di 6.000 righe di codice , senza contare gli innumerevoli bugfix che non hanno fatto altro che aggiungere righe2. Il tutto, come al solito, altamente configurabile e ridondante con il classico modulo delle collezioni , che è rimasto lì, e servito da icone così criptiche che hanno dovuto aggiungere suggerimenti testuali al passaggio del mouse per chiarire cosa significassero..

In questo codice di qualità, troveremo l’infinito while sotto lo switch case nell’if nell’if nel for (sorgente ):

 1for(int k = 0; k < num_rules; k++)
 2  {
 3    const int n = sscanf(buf, "%d:%d:%d:%d:%399[^$]", &mode, &item, &off, &top, str);
 4
 5    if(n == 5)
 6    {
 7      if(k > 0)
 8      {
 9        c = g_strlcpy(out, "<i>   ", outsize);
10        out += c;
11        outsize -= c;
12        switch(mode)
13        {
14          case DT_LIB_COLLECT_MODE_AND:
15            c = g_strlcpy(out, _("AND"), outsize);
16            out += c;
17            outsize -= c;
18            break;
19          case DT_LIB_COLLECT_MODE_OR:
20            c = g_strlcpy(out, _("OR"), outsize);
21            out += c;
22            outsize -= c;
23            break;
24          default: // case DT_LIB_COLLECT_MODE_AND_NOT:
25            c = g_strlcpy(out, _("BUT NOT"), outsize);
26            out += c;
27            outsize -= c;
28            break;
29        }
30        c = g_strlcpy(out, "   </i>", outsize);
31        out += c;
32        outsize -= c;
33      }
34      int i = 0;
35      while(str[i] != '\0' && str[i] != '$') i++;
36      if(str[i] == '$') str[i] = '\0';
37
38      gchar *pretty = NULL;
39      if(item == DT_COLLECTION_PROP_COLORLABEL)
40        pretty = _colors_pretty_print(str);
41      else if(!g_strcmp0(str, "%"))
42        pretty = g_strdup(_("all"));
43      else
44        pretty = g_markup_escape_text(str, -1);
45
46      if(off)
47      {
48        c = snprintf(out, outsize, "<b>%s</b>%s %s",
49                     item < DT_COLLECTION_PROP_LAST ? dt_collection_name(item) : "???", _(" (off)"), pretty);
50      }
51      else
52      {
53        c = snprintf(out, outsize, "<b>%s</b> %s",
54                     item < DT_COLLECTION_PROP_LAST ? dt_collection_name(item) : "???", pretty);
55      }
56
57      g_free(pretty);
58      out += c;
59      outsize -= c;
60    }
61    while(buf[0] != '$' && buf[0] != '\0') buf++;
62    if(buf[0] == '$') buf++;
63  }

e altri if annidati su 2 livelli dentro switch case necessari per supportare le scorciatoie da tastiera (sorgente ).

Quest’ultima maledetta schifezza è stata la goccia che ha fatto traboccare il vaso e mi ha fatto forkare Ansel. Mi rifiuto di lavorare su una bomba a orologeria in una squadra che non vede il problema e gioca con il codice nel tempo libero. Programmare può divertire loro, non me. E correggere le schifezze fatte da ragazzini irresponsabili con il doppio dei miei anni, specialmente quando rompono cose che avevo ripulito 3 o 4 anni fa, mi fa infuriare.

Tavolo luminoso

Il tavolo luminoso ha subito 2 riscritture quasi complete, la prima all’inizio del 2019 e la seconda alla fine del 2019, che ha aggiunto molte funzioni discutibili come la vista culling .

In breve, la modalità culling è divisa in 2 sottomodalità: dinamica e statica, che gestiscono il numero di immagini in modo diverso. Molti utenti ancora non hanno capito la differenza 4 anni dopo. Abbiamo quindi la vista predefinita (gestore di file), il tavolo luminoso zoomabile (che nessuno usa), il culling statico, il culling dinamico e la modalità di anteprima (una singola immagine a schermo intero).

Poi, vengono aggiunte più opzioni di visualizzazione alle miniature nel tavolo luminoso, che consentono di definire delle sovrapposizioni: sovrapposizioni permanenti di base, sovrapposizioni EXIF estese permanenti, le stesse ma solo al passaggio del mouse, e infine le sovrapposizioni temporizzate al passaggio del mouse (con un timer configurabile).

Il codice dell’interfaccia che renderizza le miniature e le loro sovrapposizioni deve quindi tenere conto di 5 viste diverse e 7 varianti di visualizzazione , cioè 35 combinazioni possibili. Il codice che garantisce il corretto ridimensionamento delle miniature necessita quindi di un totale di 220 righe .

Ma non finisce qui, perché il codice che renderizza la GUI delle miniature è condiviso anche con la barra inferiore «filmstrip», il che porta in realtà a 36 combinazioni possibili nel rendering delle miniature. Moltiplicato per 3 temi della GUI di diversi colori di base, si arriva a 108 insiemi di istruzioni CSS per stilizzare completamente la GUI… di cui molte sono state dimenticate nella revisione grafica di Darktable 4.0 , e come potrebbe essere altrimenti?

In Darktable 2.6, avevamo 4193 righe per il tutto, che aveva solo le viste gestore di file, tavolo luminoso zoomabile e anteprima a schermo intero, con solo 2 modalità di sovrapposizione delle miniature (sempre visibili o visibili al passaggio del mouse):

  • 2634 righe views/lighttable.c  per il tavolo luminoso e il rendering delle miniature,
  • 1124 righe in libs/tools/filmstrip.c  per la barra filmstrip, che duplica parzialmente il codice del tavolo luminoso per il rendering delle miniature,
  • 435 righe in libs/tools/global_toolbox.c , per il menu a pulsanti che consente di abilitare o disabilitare le sovrapposizioni delle miniature.

Dopo Darktable 3.0 e l’aggiunta delle modalità culling, arriviamo a 6731 righe:

Dopo Darktable 3.2 e le aggiunte delle 7 varianti di sovrapposizioni altamente configurabili e un po’ di refactoring del codice, arriviamo a 8380 righe:

In Darktable 4.2, dopo la correzione di molti bug, arriviamo a un totale di 9264 righe:

Il numero di righe (specialmente in un codice che si prende un piacere fuori luogo a ignorare le buone pratiche di programmazione) è un indicatore diretto della difficoltà di fare il debug di qualsiasi cosa lì dentro, ma anche un indicatore indiretto (nel caso specifico del codice della GUI) del carico di CPU necessario per far girare il software.

Infatti, se avvii darktable -d sql e passi il mouse su una miniatura nel tavolo luminoso, otterrai nel terminale:

 1140.8252 [sql] darktable/src/common/image.c:311, function dt_image_film_roll(): prepare "SELECT folder FROM main.film_rolls WHERE id = ?1"
 2140.8259 [sql] darktable/src/common/image.c:387, function dt_image_full_path(): prepare "SELECT folder || '/' || filename FROM main.images i, main.film_rolls f WHERE i.film_id = f.id and i.id = ?1"
 3140.8271 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 4140.8273 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 5140.8275 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 6140.8277 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 7140.8279 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 8140.8280 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 9140.8282 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
10140.8284 [sql] darktable/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (104337) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
11140.8286 [sql] darktable/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (104337) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
12140.9512 [sql] darktable/src/common/act_on.c:156, function _cache_update(): prepare "SELECT imgid FROM main.selected_images WHERE imgid=104337"
13140.9547 [sql] darktable/src/common/act_on.c:156, function _cache_update(): prepare "SELECT imgid FROM main.selected_images WHERE imgid=104337"
14140.9550 [sql] darktable/src/common/act_on.c:288, function dt_act_on_get_query(): prepare "SELECT imgid FROM main.selected_images WHERE imgid =104337"
15140.9552 [sql] darktable/src/libs/metadata.c:263, function _update(): prepare "SELECT key, value, COUNT(id) AS ct FROM main.meta_data WHERE id IN (104337) GROUP BY key, value ORDER BY value"
16140.9555 [sql] darktable/src/common/collection.c:973, function dt_collection_get_selected_count(): prepare "SELECT COUNT(*) FROM main.selected_images"
17140.9556 [sql] darktable/src/libs/image.c:240, function _update(): prepare "SELECT COUNT(id) FROM main.images WHERE group_id = ?1 AND id != ?2"
18140.9558 [sql] darktable/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (104337) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"

il che significa che vengono fatte 18 richieste SQL al database per recuperare le informazioni dell’immagine, ed eseguite ogni volta che il cursore passa su una nuova miniatura, senza motivo dato che i metadati non sono cambiati dal passaggio precedente.

In Ansel, rimuovendo la maggior parte delle opzioni, sono riuscito a risparmiare 7 richieste, il che non impedisce comunque le richieste duplicate ma migliora comunque un po’ i tempi (i timestamp sono i numeri che iniziano ogni riga):

 112.614534 [sql] ansel/src/common/image.c:285, function dt_image_film_roll(): prepare "SELECT folder FROM main.film_rolls WHERE id = ?1"
 212.615225 [sql] ansel/src/common/image.c:356, function dt_image_full_path(): prepare "SELECT folder || '/' || filename FROM main.images i, main.film_rolls f WHERE i.film_id = f.id and i.id = ?1"
 312.616499 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 412.616636 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 512.616769 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 612.616853 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 712.616930 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 812.617007 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
 912.617084 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
1012.617205 [sql] ansel/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (133727) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
1112.617565 [sql] ansel/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (133727) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"

Il problema è che il codice sorgente annida i comandi SQL all’interno delle funzioni che disegnano la GUI, e districare questo pasticcio attraverso i diversi livelli ereditati dal «refactoring» (che dovrebbe semplificare il codice, ma in realtà no) è ancora una volta archeologia. E se il problema fosse stato risolto quando il codice era di 6700 righe su 3 file, non staremmo cercando, 4 anni dopo, le cause in 2500 righe aggiuntive ora sparse in 7 file diversi (senza contare i file .h).

Siamo nel caso esemplare in cui il «refactoring» ha in realtà complicato il codice e in cui unire il codice delle miniature tra filmstrip e tavolo luminoso non ha fatto che aggiungere più if interni (rami) annidati su diversi livelli, che complicano ancora di più la struttura, solo per seguire ciecamente il principio di riuso del codice , che qui è in conflitto con il principio di modularità , che uno sviluppatore capace avrebbe risolto con l’[ereditarietà](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming) , perché anche se non è facile da fare in C, è perfettamente possibile (in effetti, Darktable usa questo principio nel codice del 2009-2010).

L’estetica prende il sopravvento sulla stabilità

Darktable 4.2 introduce l’anteprima degli stili nella camera oscura. Sarebbe fantastico se gli stili non fossero profondamente rotti, quando usati con un ordine della pipeline non predefinito e istanze multiple di moduli. Il problema è che una soluzione pulita e a lungo termine coinvolge la teoria dei grafi orientati , ed è lì che abbiamo perso i nostri amati imbrattatori di codice copiato-incollato.

Nello stesso spirito, abbiamo grandi incoerenze nel copia-incolla della cronologia in modalità sovrascrittura quando vengono usati anche i preset predefiniti dell’utente (specialmente nel modulo di bilanciamento del bianco). Ma è molto più divertente incasinare l’interfaccia, quindi resterà lì per molto tempo.

Darktable 3.6 e 3.8 hanno introdotto molte varianti dell’istogramma: vettorscopio, forma d’onda verticale, spazi colore avanzati ed esotici. Solo che se avvii darktable -d perf nel terminale e apri la camera oscura, vedrai un sacco di

123.748084 [histogram] took 0.003 secs (0.000 CPU) scope draw
223.773753 [histogram] took 0.005 secs (0.004 CPU) scope draw
323.783284 [histogram] took 0.001 secs (0.000 CPU) scope draw

ogni volta che muovi il cursore nella finestra (e nemmeno sopra l’istogramma). È l’istogramma che viene ridisegnato a ogni interazione tra cursore e finestra. Lo stesso problema affligge molti widget grafici personalizzati  e la sua causa è non identificata. Nota che non affligge Ansel, quindi la causa dovrebbe essere nascosta da qualche parte nelle 23.000 righe di codice che ho rimosso.

Due volte, ho provato a rifattorizzare lo schifo che questa funzione è diventata , ma ogni volta veniva inserita una nuova funzione più urgente che invalidava il mio lavoro. Ho semplicemente rinunciato.

Lo stato di putrefazione dell’istogramma è tale che una riscrittura completa richiederebbe meno tempo di un refactoring, tanto più che l’istogramma viene campionato troppo tardi nella pipeline, nello spazio colore dello schermo, il che rende nulla la definizione di uno spazio colore dell’istogramma dato che il gamut viene tagliato nello spazio colore dello schermo comunque. Ma indovina un po’… Darktable 4.4 avrà ancora più opzioni, con la possibilità di definire le armonie di colore (fondamentali per i geek che dipingono con i numeri e modificano gli istogrammi).

Resta il fatto che, ogni volta che muovi il cursore, viene avviato un gran numero di ricalcoli inutili per niente. Quanto è grave? Ho avuto l’idea di misurare l’uso della CPU del mio sistema quando è inattivo, con lo strumento Linux powertop. Il protocollo è piuttosto semplice: un portatile (CPU Intel Xeon Mobile di 6ª generazione), che lavora a batteria in modalità di risparmio energetico, retroilluminazione impostata al minimo, apri l’app e non tocchi nulla per 4 min, poi monitora il consumo globale di CPU del sistema riportato da powertop durante il 5° minuto:

  • Sistema di base (nessuna app aperta tranne powertop in esecuzione in un terminale): dal 3,0 al 3,5% di CPU
  • Ansel:
    • aperto sul tavolo luminoso: dal 2,9 al 3,4% di CPU,
    • aperto sulla camera oscura: dal 3,8 al 4,5% (prima di ripristinare i gruppi di moduli a Darktable 3.2),
    • aperto sulla camera oscura: dal 3,0 al 3,5% (dopo aver ripristinato i gruppi di moduli),
  • Darktable:
    • aperto sul tavolo luminoso: dal 6,6 al 7,1% di CPU,
    • aperto sulla camera oscura: dal 30,9 al 44,9% di CPU (no, non è un errore di virgola),

Non capisco cosa calcoli Darktable quando lo lasciamo aperto senza toccare il computer, perché non c’è nulla da calcolare. Darktable nel tavolo luminoso consuma da solo quanto l’intero sistema (Fedora 37 + desktop KDE + gestore di password e client Nextcloud in esecuzione in background), e consuma 10 volte tanto quanto l’intero sistema quando è aperto nella camera oscura.

Tutto ciò indica un codice dell’interfaccia grafica molto buggato. In Ansel, ho rimosso gran parte del codice sporco, senza ottimizzare nient’altro, e questi numeri non fanno che convalidare la mia scelta: il codice sporco nasconde problemi non rilevabili leggendolo, e semplicemente non possiamo continuare su questa strada.

A quanto pare sono l’unico a pensare che sia inaccettabile privare la pipeline dei pixel da un terzo alla metà della potenza della CPU per disegnare una stupida interfaccia. Comunque la si metta, non c’è alcuna ragione valida perché un software lasciato aperto senza toccarlo trasformi il computer in un tostapane, tanto più che non compriamo più il gas russo.

Lavorare contro noi stessi

Siamo fotografi. Il fatto che ci serva un computer per fare fotografia è una novità (vecchia di 20 anni), legata alla tecnologia dell’imaging digitale che ha sostituito per ogni sorta di ragioni (buone e cattive) una tecnologia vecchia di 160 anni, conosciuta e padroneggiata. Nel processo, il fatto che ci servano un computer e un software per produrre immagini è puro e semplice overhead . Costringere le persone che non capiscono i computer a usarli per svolgere compiti che prima riuscivano a gestire perfettamente a mano è anche una forma di oppressione, e mascherarla come una sorta di progresso tecnico è una forma di violenza psicologica.

Il software implica sviluppo, manutenzione, documentazione e gestione di progetto. Sono diversi strati di overhead sopra i precedenti. Eppure il fatto che la manodopera nei progetti open-source non chieda un compenso non dovrebbe nascondere il fatto che il tempo speso (perso?) sul software, sul suo uso, sul suo sviluppo, sulla sua manutenzione, è di per sé un costo non rimborsabile.

I pochi esempi qui sopra danno una panoramica della complicazione del codice sorgente, ma anche del suo degrado nel tempo in termini di qualità, perché le funzioni di base e robuste vengono sostituite da codice spaghetti , confuso e subdolamente buggato. Dietro questa questione di leggibilità, il vero problema è rendere più difficile la manutenibilità a medio termine, il che promette un futuro cupo per il progetto, con l’approvazione del maintainer.

In 4 anni che lavoro a tempo pieno su Darktable, il 2022 è il primo anno in cui mi trovo praticamente incapace di identificare la causa della maggior parte dei bug dell’interfaccia, perché la logica di funzionamento è diventata molto offuscata e il codice incomprensibile. Anche il numero di bug corretti è in costante diminuzione, sia in valore assoluto sia in proporzione alle pull request unite, mentre il volume del traffico di codice resta all’incirca costante (nota 1: i seguenti conteggi delle righe di codice includono solo file C/C++/OpenCL e XML generativi ed escludono i commenti 3) (nota 2: il numero di issue aperte è conteggiato per la durata di vita della versione precedente):

3.0 (dicembre 2019, un anno dopo la 2.6)
  • 1049 issue aperte , 66 issue chiuse / 553 pull request unite (12%),
  • 398 file modificati, 66 k inserimenti, 22 k eliminazioni, (netto: +44 k righe),
3.2 (agosto 2020)
  • 1028 issue aperte , 92 issue chiuse / 790 pull request unite (12%),
  • 586 file modificati, 54 k inserimenti, 43 k eliminazioni (netto: +2 k righe),
3.4 (dicembre 2020)
  • 981 issue aperte , 116 issue chiuse / 700 pull request unite (17%),
  • 339 file modificati, 46 k inserimenti, 23 k eliminazioni (netto: +23 k righe),
3.6 (giugno 2021)
  • 759 issue aperte , 290 issue chiuse / 954 pull request unite (30%),
  • 433 file modificati, 53 k inserimenti, 28 k eliminazioni (netto: +25 k righe),
3.8 (dicembre 2021)
  • 789 issue aperte , 265 issue chiuse / 571 pull request unite (46%),
  • 438 file modificati, 41 k inserimenti, 21 k eliminazioni (netto: +20 k righe),
4.0 (giugno 2022)
  • 632 issue aperte , 123 issue chiuse / 586 pull request unite (21%),
  • 359 file modificati, 30 k inserimenti, 15 k eliminazioni (netto: +15 k righe),
4.2 (dicembre 2022)
  • 595 issue aperte , 60 issue chiuse / 409 pull request unite (15%),
  • 336 file modificati, 14 k inserimenti, 25 k eliminazioni (netto: -11 k righe),
  • (le eliminazioni sono dovute per lo più alla rimozione del percorso SSE2 nel codice dei pixel, penalizzando le prestazioni delle tipiche CPU Intel i5/i7 a vantaggio delle CPU AMD Threadripper),
4.4 (giugno 2023)
  • 500 issue aperte , 97 issue chiuse / 813 pull request unite (12%),
  • 479 file modificati, 57 k inserimenti, 41 k eliminazioni (netto: +16 k righe),

Per rendere più facile il confronto, annualizziamoli :

  • 2019 : 1049 nuove issue, 66 chiuse, 88 k modifiche, +44 k righe,
  • 2020 : 2009 nuove issue, 208 chiuse, 166 k modifiche, +25 k righe,
  • 2021 : 1548 nuove issue, 555 chiuse, 143 k modifiche, +45 k righe,
  • 2022 : 1227 nuove issue, 183 chiuse, 84 k modifiche, +4 k righe.

Sembra che non sia il solo a trovare i bug del 2022 molto più difficili da affrontare, perché ne sono stati corretti molti meno rispetto al 2021, e finora il 2023 mostra la stessa tendenza. Il rapporto tra pull request (lavoro effettivamente svolto) e issue chiuse (problemi effettivamente risolti) è semplicemente ridicolo.

Tra Darktable 3.0 e 4.0, il codice della GUI è cresciuto del 53 %, da 49 k a 75 k righe4 (escludendo commenti e righe vuote), e ha raggiunto le 79 k righe nella 4.4. Lasciando da parte la sua scarsa qualità, non sono affatto sicuro che abbia migliorato l’usabilità del software del 53 %. In effetti, sono piuttosto convinto del contrario. In Ansel, finora ho ridotto il codice della GUI a 53 k righe rimuovendo pochissime funzioni.

Tutto questo è semplicemente troppo e troppo in fretta per un gruppo di dilettanti che lavorano la sera e nei fine settimana senza struttura né pianificazione. Il team di Darktable lavora contro sé stesso cercando di addentare più di quanto possa masticare, supportando troppe opzioni diverse, producendo codice il cui risultato dipende da troppe variabili d’ambiente, potendo interagire in troppi modi diversi. Tutto questo per evitare di prendere decisioni di progettazione che potrebbero offendere qualcuno limitando funzioni e opzioni disponibili. Dal lato dell’utente finale, ne risultano bug contestuali impossibili da riprodurre su altri sistemi, quindi impossibili da correggere del tutto.

È semplice : il lavoro svolto costa sempre più lavoro, e la manutenzione non è garantita, come mostra il declino delle issue chiuse, perché è semplicemente troppo. In un’azienda, questo è il momento in cui bisogna fermare l’emorragia prima di aver svuotato le casse. Ma un team di dilettanti tenuto a non produrre alcun risultato può sostenere una quantità infinita di perdite. Solo che il lavoro creato dal lavoro diventa più tedioso, frustrante e difficile con il passare del tempo, e gli utenti finali sono presi in ostaggio da una banda di stronzi che pensano solo a sé stessi e ne pagheranno il prezzo in termini di complessità della GUI, di carico inutile sulla CPU e nel dover reimparare come svolgere operazioni di base con il software almeno una volta all’anno.

In realtà, mi aspetto che l’attuale team di distruzione di massa trovi convenientemente sempre meno tempo libero da dedicare al progetto man mano che si rende conto di essersi incastrato in un vicolo cieco con un bilico, lasciando la propria merda ai prossimi. Ma prima si arrenderanno, meno danni causeranno.

Lo sperpero di opzioni e preferenze, che è la strategia prediletta di Darktable per (non) gestire i disaccordi di progettazione, crea casi d’uso ipercontestuali in cui nessun utente ha le stesse opzioni abilitate e in cui è impossibile riprodurre i bug in un ambiente diverso. E chiedere agli utenti di allegare il file di configurazione darktablerc alle segnalazioni di bug non aiuterebbe di più, dato che quel file conta attualmente 1287 righe praticamente illeggibili.

Bug strani e difficilmente riproducibili si accumulano, anche su computer System 76  progettati appositamente per Linux, dove non possiamo tirare in ballo problemi dei driver. Molti bug incoerenti e casuali di cui sono stato testimone mentre tenevo lezioni di editing non sono elencati sul bug tracker, ed è piuttosto chiaro che si annidano da qualche parte nei meandri dello sperpero di if e switch case che vengono aggiunti a un ritmo allarmante dal 2020.

Correggere questi bug strani e contestuali si può fare solo semplificando il flusso di controllo del programma e quindi limitando il numero di parametri utente. Ma il branco di smanettoni che agita le braccia sul progetto non ne vuole sapere e, peggio, le “correzioni” dei bug generalmente si limitano ad aggiungere altre righe per gestire i casi patologici uno per uno.

In effetti, Darktable soffre di diversi problemi :

  1. Un nocciolo duro di sviluppatori piuttosto mediocri che hanno molto tempo libero a disposizione per fare cose a caso, guidati dalle migliori intenzioni del mondo ma ignari dei danni che provocano (le persone mediocri sono sempre le più disponibili),
  2. La compiacenza del maintainer, che lascia passare codice sporco per essere gentile,
  3. Una carenza critica di competenze in matematica pura, algoritmica, elaborazione del segnale, scienza del colore e in generale nel pensiero astratto, che sono necessarie oltre al codice di elaborazione dei pixel per semplificare e fattorizzare le funzioni,
  4. Una spregevole abitudine di “sviluppare” copiando e incollando codice preso altrove nel progetto o in altri progetti FLOSS che magari usavano un’architettura di pipeline diversa, ma senza adattarlo di conseguenza (adattare implica capire, ed è troppo chiedere…),
  5. Un netto e categorico rifiuto di potare le funzioni per fare spazio a quelle nuove e mantenere un certo equilibrio,
  6. Un bias di campionamento, per cui gli unici utenti che interagiscono con lo sviluppo tramite Github sono programmatori e anglofoni. Il fatto è che il pubblico generale non capisce cosa sia una code forge ed è difficile incoraggiare chi non programma ad aprire un account Github per segnalare bug. Stiamo parlando di un campione di utenti composto per oltre il 44 % da programmatori  e per oltre il 35 % da persone laureate (che sono rispettivamente il 6 % e il 15 % nella popolazione generale).
  7. Uno stile di sviluppo a marce forzate, senza pianificazione né dialogo, in cui ogni utente Github può inquinare le discussioni con un’opinione non qualificata sul lavoro in corso. Il fatto è che l’elaborazione delle immagini sembra facile ed è innocua, tanto che chiunque sia in grado di calcolare un logaritmo si sente competente. Ma gli errori fondamentali nella catena colorimetrica di Darktable sono lì a ricordarci ogni giorno il contrario.
  8. Una mancanza di priorità a livello di progetto riguardo a quali funzioni rifattorizzare, stabilizzare o estendere : tutti i progetti sono aperti nello stesso momento, anche se sono in conflitto tra loro.
  9. Una quantità di attività (email e notifiche) impossibile da seguire, tra commenti, discussioni fuori tema, bug che non lo sono, proposte di modifica al codice, modifiche effettive al codice che possono avere un impatto sul tuo stesso lavoro in corso, il che significa che devi stare dappertutto tutto il tempo ; c’è molto da leggere, pochissimo da tenere, la discussione fine a sé stessa ostacola la produttività e la mancanza di una struttura di lavoro è la causa principale di tutto ciò,
  10. Bug non bloccanti nascosti in fretta prima ancora di aver finito di capirli, invece di correggerli davvero e affrontarli alla radice, il che sposta o addirittura aggrava i problemi a lungo termine senza lasciare traccia in alcun tipo di documentazione,
  11. Un calendario di rilascio che manteniamo a qualunque prezzo anche quando non è realistico, sebbene nessuno ce lo imponga,
  12. Modifiche al codice che possono avvenire in qualsiasi momento e ovunque, il che significa che lavoriamo sulle sabbie mobili e che dobbiamo lavorare tanto velocemente e tanto male quanto gli altri per non restare indietro rispetto al volume e alla frequenza delle modifiche (commit),
  13. Nuove funzioni che degradano l’usabilità e complicano l’uso senza risolvere un problema definito, che servono da progetti ricreativi per sviluppatori non formati in progettazione/ingegneria.

Ma la cosa più esasperante è questa ostinazione a sostituire funzioni semplici e funzionali con orrori di sovraingegnerizzazione destinati a compiacere usi devianti e marginali, rendendo al contempo la vita di tutti più difficile con liste infinite di opzioni sconsiderate. Il posto migliore per nascondere un albero è in mezzo alla foresta, e molti ancora non l’hanno imparato.

Confondere l’agitazione con l’attività

A ogni elettore piace criticare la deriva che, in politica, consiste nell’emanare leggi di circostanza, mal scritte, per placare l’opinione pubblica dopo un evento particolare, per ostentare che agiamo, mentre leggi simili esistono già e non vengono applicate, o non del tutto, per mancanza di mezzi. Questo lo chiamiamo agitazione : sembra azione, suona come azione, ha il costo dell’azione, ma non porta a nulla di tangibile o pratico.

Il team di dilettanti senza gestione del progetto che si agita attorno a Darktable produce solo problemi futuri. In passato, Darktable veniva rilasciato una volta all’anno con circa 1500-2000 commit  in più rispetto alla versione precedente. Questo è ormai il volume di modifiche realizzato in 6 mesi. Un volume di “lavoro” che aumenta così in fretta senza portare a metodi di lavoro di squadra, comprese priorità chiare per ogni versione e ripartizione dei compiti, e senza controllo di qualità del software basato su metriche oggettive (numero di passaggi o tempo trascorso per portare a termine un dato compito), è solo un gruppo di tizi che si pestano i piedi a vicenda mentre portano avanti la propria agenda senza curarsi degli altri, né del progetto, né degli utenti.

Darktable è diventato il club informatico del liceo, dove gli smanettoni si divertono. Nel complesso è un riassunto di tutte le peggiori storie delle aziende informatiche, con la differenza che il progetto non guadagna un centesimo, il che rende urgente chiederci perché ce lo imponiamo : non ci sono profitti da spartire, ma tutti ne condividono i costi. È un ambiente di lavoro caotico e tossico che non farebbe che fabbricare burn-out se i dilettanti part-time fossero tenuti a produrre risultati e dovessero lavorare a tempo pieno. Essendo l’unico a lavorarci a tempo pieno, ti lascio immaginare la quantità di stress e di energia sprecata per restare aggiornato con la cacofonia permanente, solo per essere sicuro di non perdere il 2 % che mi riguarda davvero nella quantità di rumore prodotta da discussioni non regolamentate.

Dal lato utente, si elogia l’effervescenza del progetto Darktable (sì, c’è movimento), senza rendersi conto che la corsa sfrenata dei commit non è attività ma agitazione, e in particolare debito tecnico che dovrà essere pagato non-sappiamo-quando da non-sappiamo-chi. La bellezza di un progetto in cui nessuno deve assumersi la responsabilità delle proprie cazzate perché nessuno risponde di nulla : è scritto nella licenza GNU/GPL. Possiamo quindi rovinare il lavoro dei precedenti in totale impunità.

Abbiamo l’inizio di un controllo di qualità, tramite i test di integrazione, che misurano l’errore percettivo su elaborazioni di immagini di riferimento, ma non innescano alcuna reazione quando vediamo un delta E medio di 1,3 (piccolo) quando la natura della modifica avrebbe dovuto avere un delta E rigorosamente nullo. Se l’unico test passa (perché testiamo una singola immagine SDR su uno scatto in studio), non ci si pone alcuna domanda sul fatto che la teoria sia solida e robusta. Abbiamo trasformato il test in una discolpa, purché la metrica resti sotto la soglia di validazione…

Con l’uscita di Darktable 4.0 — Geektable —, ho visto su Youtube gente cominciare a lamentarsi che questa versione non era molto entusiasmante. Dopo anni passati a drogare la gente con versioni superlative, zeppe di nuove funzioni che non abbiamo il tempo di testare come si deve (6-8 tizi che pisciano 38-50 k righe ogni 6 mesi lavorando solo la sera e nei fine settimana, stai ancora sognando ?), li abbiamo resi dipendenti dall’Albero di Natale sovraccarico per assicurarci che, il giorno in cui cominceremo a essere responsabili e a rilasciare versioni stabili (quindi noiose), questo ci verrà rinfacciato.

La ragione di questo ritmo frenetico di rilasci è che le pull request più vecchie di 3 mesi sono sistematicamente in conflitto con il branch master, dato che quest’ultimo viene scosso ogni mese per “test generalizzati”. Ma i rari utenti che compilano il branch master non hanno idea di cosa debbano testare in particolare, a meno che non abbiano sezionato la cronologia dei commit di Git, il che implica capire sia il linguaggio C sia l’impatto delle modifiche nella pratica sul software.

Per limitare i branch di lunga durata, che finiranno immancabilmente in conflitto con il master, abbiamo trovato una soluzione geniale : rilasciamo 2 versioni all’anno, facendo dello sviluppo a marce forzate basato su codice incompiuto e appena testato uno stile di vita, senza mai renderci conto che il problema di fondo è anzitutto la mancanza di pianificazione, ma anche che i collaboratori iniziano a scrivere codice prima di aver finito di definire il problema da risolvere (quando c’è un vero problema da risolvere, e non solo un tizio che si è svegliato pensando “sarebbe figo se…”), lavorando in parallelo su entrambe le parti senza comunicazione.

La polvere non ha il tempo di posarsi che già stiamo scuotendo di nuovo la base di codice, senza imporre fasi di stabilizzazione in cui ci limitiamo a ripulire i bug (e non parlo del mese di feature-freeze precedente al rilascio, ma di versioni dedicate esclusivamente alla pulizia del codice). Il bug tracker implode nelle 3 settimane successive a ogni rilascio, perché una parte significativa degli utenti usa solo i pacchetti precompilati, che coincidono con le vacanze di Natale ed estive, in cui personalmente ho di meglio da fare dopo lo sprint già stressante che è il mese precedente al rilascio.

Dal 2021, quando aggiorno il mio repository Git con le ultime modifiche del master di Darktable, mi chiedo sempre cosa hanno rotto questa volta. Rompiamo più velocemente di quanto correggiamo, e il più delle volte le correzioni rompono qualcos’altro. Gli unici utenti che trovano Darktable stabile sono in realtà quelli che ne fanno un uso molto basilare, il che è ironico per un’app il cui punto di forza è essere avanzata.

E poi mi si serve il fatto che è lavoro gratuito come se fosse una scusa. Ma in realtà è un’aggravante : perché ci imponiamo condizioni di lavoro simili se non è nemmeno redditizio ??? Oltre al fatto che questo lavoro gratuito mi procura un burn-out post-rilascio all’anno, costa sempre di più in manutenzione e la manutenzione è sempre più spregevole da fare. Non è lavoro gratuito, è peggio : è lavoro che costa senza pagare.

Comunque sia, il lavoro è fornito da persone che hanno tempo ed energie limitate. Se la risorsa è limitata, basta con le stronzate : siamo negli stessi vincoli di redditività di un’azienda, meno i contributi sociali, salvo che la nostra moneta di scambio è il tempo e non è rimborsabile. Senza gestione delle priorità, verremo travolti da un debito tecnico che non avremo le risorse per mantenere.

Cosa aspettiamo a essere felici ?

Il fatto che Darktable sia un rullo compressore in corsa impazzita e senza autista, che genera una quantità di lavoro crescente, è un cattivo segnale per un progetto di 15 anni. Normalmente, un progetto maturo rallenta perché è abbastanza completo da essere utilizzabile e perché le persone che ci lavorano hanno trovato la loro velocità di crociera e metodi di lavoro efficienti.

Ci lavoro a tempo pieno dal 2018, per un reddito mensile tra gli 800 e i 900 €, ed è un eufemismo dire che è mal pagato per sopportare le disastrose conseguenze di dilettanti disorganizzati che cercano di divertirsi a scapito della qualità del prodotto finale e della sua usabilità da parte dei Babbani dell’informatica. Peraltro, i Babbani vengono anche disprezzati, per principio.

Se mi rintano sotto una pietra per un mese per sviluppare uno spazio colore percettivo, quando ne esco è per scoprire il nuovo sistema labirintico che viola un po’ di più il paradigma view-model-controller e sentirmi dire che arrivo troppo tardi per oppormi. Se prendo 3 settimane di vacanza ad agosto, è per scoprire che il maintainer ha aggirato (una volta di più) la mia revisione su una modifica matematica riguardante il suddetto spazio colore, che richiede di sedersi con calma e riflettere, tutto questo perché… bisognava muoversi in fretta ? Per quale emergenza, esattamente ?

Probabilmente ho ricevuto la notifica da qualche parte in mezzo alle 2234 email che Github mi ha inviato tra gennaio e agosto 2022 (nel 2021 erano 4044), senza contare gli utenti che mi pingano dappertutto, su Youtube, Reddit, Matrix, Github, Telegram, direttamente via email, e in precedenza su pixls.us (693 email nel 2022, 948 nel 2021). Tutto questo da parte di gente completamente fuori sincrono che non si rende conto che io faccio questo tutta la settimana, che la fotografia può essere il loro hobby ma è il mio lavoro, e che gradirei semplicemente che la gente mi lasciasse in pace durante i fine settimana e le vacanze. Puoi immaginare che i più fastidiosi non sono quelli che sostengono finanziariamente il mio lavoro. La gente rispetta il lavoro solo se le è stato fatturato a caro prezzo.

Non ho tempo di fare il ricercatore, il designer, oltre che il segretario, mentre faccio da baby-sitter tecnico a un team di Gastone Paperoni che hanno bisogno sia di essere formati sia di essere sorvegliati perché incapaci di :

  1. fare una pianificazione dello sviluppo con una lista di priorità delle nuove funzioni su cui lavorare,
  2. fornire un capitolato di specifiche dei bisogni e dei problemi, con un caso d’uso reale, prima di precipitarsi sul loro editor di codice e fare qualunque cosa pur di sviluppare una nuova funzione in cerca di un problema da risolvere.
  3. valutare il costo di manutenzione della modifica prima di inventare la pompa per bacon bionica a osmosi inversa che funziona solo nei giorni pari se Giove è in opposizione di fase con Saturno,
  4. limitare le spese e tagliare le perdite quando si incastrano in vicoli ciechi di progettazione che introducono regressioni peggiori dei benefici ipotetici attesi,
  5. prendersi la responsabilità e rimandare un rilascio se il codice è palesemente non pronto (oppure non ho capito nulla e gli azionisti chiederanno le nostre teste se rilasciamo in ritardo ???).

La gestione (o gestione del team) è un sovraccarico che costa un po’ di lavoro, ma il team di Darktable ha raggiunto una scala in cui la mancanza di gestione costa in realtà più lavoro, tanto più che nessuno dei fondatori del progetto è ancora nel team e i progetti iniziali di design devono essere ricostruiti con il reverse engineering a colpi di grep nel codice ogni volta che qualcosa ha bisogno di essere cambiato. Questo era sostenibile con un team ridotto in cui tutti si conoscevano, ma Darktable è diventato un progetto ad alto traffico durante i lockdown del Covid e questo modo di lavorare non è sostenibile con il personale attuale.

Basta andare a vedere il codice ! Confronta il branch darktable-2.6.x con darktable-4.2.x, file per file, e divertiti !

Tutto quello che ho sentito finora sono frasi preconfezionate del tipo “è come molti altri progetti opensource” e “non c’è niente che possiamo fare”. La gente è spaventata dalla quantità di lavoro che comporta un fork (ho ricevuto email che cercavano di convincermi che dividesse la produttività), senza rendersi conto della quantità di risorse attualmente sprecate dal progetto Darktable e dello stress permanente di dover basare il proprio lavoro su una base di codice instabile scossa in continuazione. Finora, Ansel mi è costato meno fatica e ho risolto un numero significativo di problemi tra cui alcuni segnalati fin dal 2016 senza segni di interesse da parte della maledetta “comunità”.

Inoltre, Ansel fornisce pacchetti nightly compilati automaticamente per Linux (.AppImage) e Windows (.exe), in modo da permettere veri test generalizzati, anche da parte di persone incapaci di compilare il software da sole. L’avevo chiesto già nel 2019 , ma a quanto pare gli smanettoni hanno di meglio da fare, e ho dovuto investire io stesso 70 h per far sì che accadesse. L’operazione è già un successo e ha permesso di correggere in pochi giorni bug di Windows che avrebbero richiesto settimane per essere individuati in Darktable. (E Darktable si è appropriato 3 settimane dopo del mio script di compilazione AppImage senza dovuti crediti, ma è un dettaglio).

A proposito di produttività, ricordiamo che il tavolo luminoso è stato riscritto quasi interamente due volte dal 2018 (e l’ultima versione non è né migliore né più veloce) e che la grande modifica ai filtri delle collezioni introdotta ad aprile 2022  ha sovrascritto un’altra modifica simile (ma che usava solo 600 righe invece di 6000) introdotta a febbraio 2022  (la versione di febbraio è quella presente in Ansel). Non possiamo decentemente pronunciare la parola “produttività” quando il lavoro di un collaboratore cancella letteralmente il lavoro precedente di un altro nell’arco di un mese, per semplice mancanza di gestione del progetto. Questo si chiama pestarsi i piedi a vicenda.

E allora cosa facciamo per risolvere il problema ? Soffrire in silenzio ? Vivere nel negazionismo ? Continuare a correggere cose che un altro romperà l’anno prossimo quando non staremo guardando ? Continuare a far credere al Babbano, a lunghezza di forum di fotografia, che l’opensource è buono tanto quanto il proprietario, tenendo come jolly il fatto che è gratuito quindi non hai il diritto di lamentarti ? Non è un po’ troppo facile e disonesto, questo doppio discorso ?

Non ti piacerebbe smetterla di far passare le abitudini per esperienza e di confondere il fatalismo con la saggezza, ma piuttosto affrontare il problema alla radice ? Non pensi che tu e io meritiamo di meglio di un software progettato da dilettanti il cui unico talento è avere tempo libero e possono permettersi di lavorare gratis da quando sono passati alla dirigenza e i figli sono partiti per l’università ?

Oppure mi sono sbagliato fin dall’inizio, e l’opensource consiste nel dare strumenti troppo complicati a smanettoni che non ne hanno davvero bisogno, cercando al contempo di convincere il resto del mondo che l’open source non è un’ipernicchia per sviluppatori ?

Quattro anni di lavoro per arrivare a questo punto

Dopo 4 anni di lavoro a tempo pieno su Darktable per il 70 % del salario minimo, e 2 anni a sopportare la cronica insoddisfazione di macchiare il mio nome contribuendo a della merda, ho forkato Ansel e non tornerò indietro.

In 4 anni, ho portato a questo software qualcosa che mancava dolorosamente : un flusso di lavoro unificato, basato su un insieme di moduli progettati per lavorare insieme, ma che agiscono ciascuno su un aspetto distinto, laddove i moduli di Darktable erano piuttosto una raccolta di plugin disparati. Stiamo parlando di :

  • filmic,
  • equalizzatore dei toni,
  • il modulo delle sfocature fisicamente accurate,
  • entrambe le versioni del bilanciamento del colore,
  • la calibrazione del colore, inclusa la GUI per profilare con i color checker direttamente in camera oscura e il bilanciamento del bianco che usa gli standard CIE,
  • il modulo negadoctor per invertire i negativi delle pellicole basandosi su Kodak Cineon,
  • il modulo diffusione e nitidezza per l’aggiunta e la rimozione della sfocatura basato sulla diffusione termica,
  • la ricostruzione delle alte luci con i guided laplacian.

Ho anche sviluppato strumenti più fondamentali che forniscono le basi per i moduli precedenti :

  • un risolutore di equazioni differenziali alle derivate parziali anisotrope del 4° ordine nello spazio delle wavelet per diffusione e nitidezza,
  • un adattamento del precedente come guided laplacian per la ricostruzione del segnale RGB tramite propagazione dei gradienti,
  • un modello percettivo dell’aspetto del colore che tiene conto dell’effetto Helmholtz-Kohlrausch nel calcolo della saturazione, per limitare l’effetto “fluo” che tipicamente accompagna le impostazioni di saturazione intense, nel bilanciamento del colore,
  • un aiuto teorico nello sviluppo del filtro guidato invariante all’esposizione (EIGF), nell’equalizzatore dei toni,
  • un risolutore di equazioni vettoriali lineari con il metodo di Choleski,
  • vari metodi di interpolazione di ordine 2, 3 e basati su funzioni radiali.

Nella GUI, ho fatto in particolare :

  • rifattorizzare la dichiarazione degli stili, rimuovendo lo styling dal codice C per mapparlo sul foglio di stile CSS, permettendo di avere temi multipli per l’interfaccia, compresi quelli definiti dall’utente,
  • introdurre la modalità di anteprima focus-peaking  e la modalità di valutazione del colore ISO 12 646 ,
  • introdurre il vocabolario dei colori nel selettore globale del colore , permettendo di nominare il colore selezionato a partire dalle sue coordinate di cromaticità, rivolgendosi ai fotografi daltonici.

Dopodiché, ho scritto decine di pagine di documentazione in 2 lingue, pubblicato articoli e decine di ore di video su YouTube per mostrare come usare i moduli, in quale contesto e con quale beneficio, comprese modifiche rapide che usano solo da 3 a 5 moduli per elaborare dal 75 all'80 % delle foto, indipendentemente dalla loro gamma dinamica. Nel mondo dell’open-source, tranne forse per i progetti sostenuti da fondazioni (come Krita e Blender), questo livello di supporto e documentazione semplicemente non esiste, e non sono gli sviluppatori stessi a occuparsi di questo lavoro.

Nonostante tutto questo, non ho mai avuto più di 240 donatori, da confrontare con circa 1800 rispondenti unici che hanno partecipato ai sondaggi Darktable del 2020 e del 2022 , e che dichiarano di spendere tra i 500 e i 1000 €/anno  in fotografia.

Non starò a guardarli mentre distruggono l’usabilità di questo software cercando di convincermi che è progresso e che non c’è niente da fare. Invece del progresso, è la visione delirante del progresso da parte di un gruppo di cinquantenni dilettanti. Sono 2 anni che sto zitto pazientemente, cercando di essere gentile, ma guardando il degrado delle funzioni di base, complessificate per assecondare le manie di programmatori impazziti, avrei dovuto essere spregevole prima. Fare il gentile non ha risolto nulla, perché la tendenza non solo è proseguita, ma ha accelerato, e non ci sarà alcuna presa di coscienza prima del punto di non ritorno. Non possiamo aspettarci che chi ha creato i problemi sia quello a risolverli.

Quindi, se devo lavorare per una “comunità” che è coinvolta soprattutto per l’aspetto senza abbonamento del software, e che ha deciso che il mio lavoro non vale il salario minimo, ebbene, lo farò alle mie condizioni e secondo i miei standard.

In termini di funzioni, Darktable ne ha già troppe e dobbiamo potare. Sono 10 anni che lo uso ed è già stato riempito di roba mal progettata. La sfida ora è presentare le funzioni in modo intelligente, e correggere i bug fastidiosi prima che questi idioti ne introducano di nuovi, o addirittura li correggano a modo loro : nascondendo la polvere sotto il tappeto. Tenendo presente che la pipeline di Darktable ha 15 anni, e non possiamo ottimizzarla molto più di così, dato che è già stata torturata parecchio per evitare una riscrittura completa (e una riscrittura completa non offre alcun beneficio se dobbiamo mantenere Gtk come backend grafico, dato che è il principale collo di bottiglia delle prestazioni).

Le soluzioni di cui Darktable ha bisogno implicano rimuovere codice e opzioni, non aggiungerne sempre di più. La robustezza ha questo prezzo. Il team di Darktable fa l’esatto contrario senza imparare dai propri errori.

Con Ansel, voglio un modo per finire questo lavoro in pace affinché gli utenti Linux abbiano uno strumento affidabile, coerente e performante per la loro fotografia artistica. Prima di passare a Vkdt  perché il design attuale mostra i suoi limiti.


Translated from English by : Claude. In case of conflict, inconsistency or error, the English version shall prevail.

  1.  By the way, I can no longer bare the attempt of being different for the sake of it by writing “Darktable”, proper noun, without initial capital. It’s childish, it’s neither funny or disruptive, and it makes a mess of freedesktop.org  menus where the capital is anyway added to follow the standard. ↩︎

  2. The fact that bugfixes systematically add more lines of code instead of modifying existing lines is a a concerning smell that the programming logic is bad and induces too many particular cases. Rigorous programmers always try to keep their code as generic as possible to avoid spaghetti code ↩︎

  3.  You don’t need to trust me, the command to reproduce the stats is git diff release-3.4.0..release-3.6.0 --shortstat -w -G'(^[^\*# /])|(^#\w)|(^\s+[^\*#/])' -- '*.c' '*.h' '*.cpp' '*.xml.in' '*.xsl' '*CMakeLists.txt' '*.cl' ↩︎

  4. git checkout release-3.0.0 & cloc $(git ls-files -- 'src/views' 'src/gui' 'src/bauhaus' 'src/dtgtk' 'src/libs') ↩︎