Récapitulatif des épisodes précédents
- Entre 2020 et 2022, Darktable a subi une entreprise de destruction massive , par une poignée de gars avec plus de temps libre et de bienveillance que de réelles compétences,
- En 2022, j’ai commencé à remarquer un décalage ennuyeux entre les interactions GUI avec les curseurs et feedback/ mise à jour de ces curseurs. En l’absence de feedback indiquant que le changement de valeur était enregistré, les utilisateurs pouvaient le changer à nouveau, lançant ainsi des recomputes supplémentaires de pipeline et gelant effectivement leur ordinateur car le GUI stupide n’a jamais dit “je t’ai eu, attends un peu maintenant”.
- J’ai découvert que les ordres de recompte de pipelines étaient émis deux fois par clic (une fois sur les événements “bouton enfoncé”, une fois sur les événements “bouton relâché”), et encore une fois pour chaque mouvement de souris, mais aussi que les états GUI étaient mis à jour apparemment après le recomputée de pipeline.
- J’ai corrigé cela en réécrivant presque complètement les contrôles GUI personnalisés (lib Bauhaus). Je pensais que la prévention des ordres de recomputation inconsidérés allait résoudre le décalage : ce n’était pas le cas. Ensuite, j’ai découvert que la demande d’une nouvelle recompte de pipeline avant la fin de la précédente attendait que la précédente se termine, malgré un mécanisme d’arrêt mis en œuvre il y a de nombreuses années qui aurait dû fonctionner.
- J’ai corrigé cela en mettant en œuvre un mécanisme de bouton d’arrêt sur les pipelines, suivant les commentaires dans le code datant des années 2010 et des fonctions internes qui n’ont peut-être jamais fonctionné. Cela ne fonctionnait pas toujours car l’ordre d’arrêt venait souvent avec un délai notable. Encore une fois, le décalage GUI n’était pas corrigé.
…
Épisode 5 : payer la dette technique
Ce que j’ai découvert devrait vraiment figurer dans les manuels d’informatique au chapitre sur ce qu’il ne faut pas faire si vous voulez écrire une application semi-fiable.
Donc, chaque fois qu’un paramètre de traitement d’image était modifié dans un module, une demande était envoyée pour ajouter une nouvelle entrée d’historique dans la base de données (souvent plus d’une fois par interaction, comme indiqué ci-dessus). Les entrées d’historique ne sont rien d’autre qu’un instantané des paramètres internes d’un module (y compris les masques). Si un changement était détecté par rapport à l’entrée d’historique précédente, un indicateur PIPELINE_STATE
était défini sur la valeur DIRTY
pour indiquer que le pipeline nécessiterait une recompte et un gtk_widget_queue_draw()
était envoyé ce qui, comme le nom le suggère, demande à Gtk de redessiner l’aperçu principal de la chambre noire et la vignette de navigation, mais de manière asynchrone (comprenez : chaque fois qu’il trouve le temps, après que tout ce qui a été démarré précédemment soit terminé). Cela aura son importance plus tard.
Il m’a fallu un très long temps pour comprendre comment le pipeline était réellement lancé, car aucun des codes attachés aux modules et au pipeline ne contenait quoi que ce soit disant “aller calculer ça”. En d’autres termes, aucun des codes de module ne contenait d’instruction explicite de recompte.
J’ai dû rétro-ingénérer le code de pipeline depuis l’autre extrémité, à la recherche de la manière dont le pipeline pourrait être démarré et en essayant chaque option, jusqu’à ce que je comprenne l’indicible : la première génération de développeurs de Darktable avait câblé une fonction de rappel au événement redraw
sur l’aperçu principal de la chambre noire et la vignette de navigation, mais dans un endroit complètement sans rapport dans le code. Dans ce rappel GUI, la valeur de l’indicateur PIPELINE_STATE
était vérifiée, et soit envoyait le pixmap du tampon de fond directement au widget si l’indicateur était VALID
soit demandait un recompte du pipeline si l’indicateur était DIRTY
, et cette recompte elle-même demandait un gtk_widget_queue_draw()
à l’achèvement.
Cette méthode a un mérite : c’est un codage paresseux. Ensuite, elle a une quantité astronomique d’inconvénients et de problèmes :
- elle n’est pas favorable aux développeurs, surtout dans un projet logiciel où la recherche de code et les commentaires sont toute la doc que nous pouvons espérer. Cela a pris de nombreuses heures pour comprendre la logique à travers l’archéologie des programmes. Si une commande est émise, je veux lire
command_issued()
au bon endroit dans le code, car le C est déjà suffisamment difficile à suivre sans ajouter des devinettes dans le débogage. - puisque
gtk_widget_queue_draw()
(appelé deux fois dans le pire des cas) n’est ajouté que dans la file d’attente et traité de manière asynchrone, cela ajoute le retard que Gtk pourrait souffrir (tout en traitant d’autres morceaux de l’interface graphique ou des cadres précédents) avant qu’un recompte du pipeline ne soit seulement commencé, ce qui est inutile puisque le pipeline vit dans son propre thread en parallèle, - le formidable gratin MIDI, écoutant pour les événements de pointage, de claviers et MIDI pour distribuer les raccourcis, semblait avoir surchargé le GUI global avec des écouteurs parcourant toutes les combinaisons de raccourcis connues, ce qui rendait Gtk tellement à la traîne qu’il est devenu perceptible,
- cela empêche tout mécanisme d’arrêt d’être utile, à la fois en raison des délais et parce que les lectures de drapeaux étaient croisées avec des verrouillages de threads (et des conditions de course). De plus, attendre de récupérer le verrou (mutex) de thread du pipeline bloquerait le thread GUI pendant le temps correspondant, ce qui était probablement l’une des causes du décalage du curseur avant la mise à jour de sa position,
- les appels enchaînés au rappel
redraw
pargtk_widget_queue_draw()
, ont promu des boucles de hoquet “sans fin” de redessins (inutiles) intermédiaires qui semblaient toucher davantage les personnes avec des ordinateurs lents que celles avec des bêtes puissantes. Celles-ci étaient particulièrement difficiles à reproduire, en fonction de la performance matérielle, alors vous pouvez trouver des forums où des gens sont convaincus que Darktable est le logiciel le plus lent jamais tandis que d’autres rapportent une excellente performance.
J’ai donc corrigé toute la logique :
- en rendant le rappel
redraw
stupide (dessin de n’importe quel tampon pixmap disponible, inconditionnellement), - en gérant explicitement les recomptes de pipeline dans le code du module et de l’historique, avec les recomptes de pipeline demandant un redessinement du widget à l’achèvement du pipeline (oui, c’est plus de code, et c’est fastidieux, mais maintenant vous pouvez optimiser manuellement les recomptes — les performances comptent),
- en supprimant le traitement spécial des “doublons” d’éléments d’historique (menant à une certaine pollution lors de la gestion des masques, cela devra être corrigé plus tard).
Vous pourriez penser que c’était un problème résolu et un travail bien fait, mais c’est laisser de côté les génies de Darktable.
Voyez, les modules crop et perspective sont des modules spéciaux : les ouvrir active un “mode d’édition” qui désactive toute découpe afin d’afficher l’image complète. Cela est nécessaire pour déplacer le cadre de découpe (ou ajuster d’autres positionnements) depuis l’aperçu principal, sur l’image originale complète. Le problème était qu’il n’y avait aucun moyen explicite de demander un recalcul de la chaîne de traitement… à part ajouter un nouvel élément d’historique. Ainsi, les modules ajoutaient un faux élément d’historique (plus tard rétabli) uniquement pour invalider la chaîne et appeler la fonction gtk_widget_queue_draw()
. Mais cela polluait alors la pile d’historique avec des étapes “vides”, de sorte qu’une autre personne a ajouté un traitement spécial qui fusionnait les étapes de l’historique si aucun changement de paramètre ne se produisait. Cependant, la pile d’historique (du module historique, telle que stockée dans la base de données) ne suit pas la pile d’historique annuler/rétablir, entraînant une confusion chez les utilisateurs quant à ce que annuler/réparer fait réellement.
Et voici, mesdames et messieurs, comment un mauvais design incite à un design encore pire dans une expansion incessante de folie.
Souvenez-vous que tout cela découle de la nécessité de faire fonctionner le commutateur d’arrêt d’urgence de la chaîne de traitement, pour que vous puissiez interrompre un recalcul en milieu de processus lorsque vous savez que son résultat sera de toute façon ignoré. J’ai donc dû déplacer la demande de recalcul hors du code Gtk et l’appeler partout où c’était nécessaire. Mais ensuite, j’ai dû reconfigurer la logique de mise à jour de la chaîne de traitement dans les modules crop, perspective et rotation, liquéfier et borders, et je dois encore réparer retouche (qui est le pire casse-tête du lot).
Outre la clarté de lecture et la possibilité d’optimiser les appels, la logique actuelle démarre également la chaîne en dehors du thread GUI, sans attendre que Gtk trouve le temps de redessiner le cadre. Comme d’habitude, les personnes avec un CPU de folie ne remarqueront que peu ou pas d’avantages en termes de performance, ce qui est probablement la raison pour laquelle c’est un non-problème pour l’équipe de Darktable en premier lieu.
Épisode 6 : payer les intérêts de retard de la dette technique
À ce stade, j’avais rendu les recalculs de la chaîne explicites depuis les modules et les contrôles GUI, et les avais distribués avec parcimonie (ce qui est l’avantage de les dispatcher explicitement). Et pourtant, j’ai remarqué que jouer avec des modules arrivant tard dans la chaîne était lent. En fait, lancer ansel -d perf
a montré que
toute la chaîne de traitement, à partir du module démoisaillage, était recalculée même si j’interagissais avec un module tardif prenant son entrée de équilibre des couleurs.
À ce stade, j’avais rendu les recalculs de la chaîne explicites depuis les modules et les contrôles GUI, et les avais distribués avec parcimonie (ce qui est l’avantage de les dispatcher explicitement). Et pourtant, j’ai remarqué que jouer avec des modules arrivant tard dans la chaîne était lent. En fait, lancer ansel -d perf
a montré que
toute la chaîne de traitement, à partir du module démoisaillage, était recalculée même si j’interagissais avec un module tardif prenant son entrée de équilibre des couleurs.
À ce stade, j’avais rendu les recalculs de la chaîne explicites depuis les modules et les contrôles GUI, et les avais distribués avec parcimonie (ce qui est l’avantage de les dispatcher explicitement). Et pourtant, j’ai remarqué que jouer avec des modules arrivant tard dans la chaîne était lent. En fait, lancer ansel -d perf
a montré que
toute la chaîne de traitement, à partir du module démoisaillage, était recalculée même si j’interagissais avec un module tardif prenant son entrée de équilibre des couleurs.
Darktable a toujours eu un cache de pixels. Il stocke essentiellement les états intermédiaires de l’image, entre les modules. Ainsi, commencer des recalculs de la chaîne bien en dessous du module actuel signifiait qu’il était principalement inutile. Il s’est avéré que le cache n’utilisait que 8 lignes de cache, ce qui est vraiment un sous-emploi des quantités folles de RAM d’aujourd’hui. Mais passer à 64 n’a pas aidé avec les échecs de cache : le cache était toujours principalement inutile, et la plupart de la chaîne était encore recalculée.
Nous devons faire une pause ici. Même un ingénieur mécanicien sans formation en programmation comme moi sait ce qu’est un cache LRU :
- vous créez une liste fixe d’emplacements (lignes de cache),
- une fois que vous avez quelque chose à mettre en cache, vous allouez un tampon mémoire de taille prédéfinie à l’un de ces emplacements et lui attribuez un identifiant unique. Cela pourrait être un checksum, un hash aléatoire ou même un horodatage, il suffit juste de le cuisiner de la même manière pour obtenir quelque chose d’unique,
- lorsque vous avez besoin des données associées à un identifiant unique, vous interrogez la liste des emplacements et recherchez si cet ID est connu :
- si c’est le cas, vous récupérez son tampon associé,
- si ce n’est pas le cas :
- si vous avez encore des emplacements vides, vous créez le tampon associé et copiez les données pour une réutilisation ultérieure,
- sinon, vous videz l’emplacement le plus ancien et le réutilisez pour héberger vos nouvelles données.
- vous créez une liste fixe d’emplacements (lignes de cache),
- une fois que vous avez quelque chose à mettre en cache, vous allouez un tampon mémoire de taille prédéfinie à l’un de ces emplacements et lui attribuez un identifiant unique. Cela pourrait être un checksum, un hash aléatoire ou même un horodatage, il suffit juste de le cuisiner de la même manière pour obtenir quelque chose d’unique,
- lorsque vous avez besoin des données associées à un identifiant unique, vous interrogez la liste des emplacements et recherchez si cet ID est connu :
- si c’est le cas, vous récupérez son tampon associé,
- si ce n’est pas le cas :
- si vous avez encore des emplacements vides, vous créez le tampon associé et copiez les données pour une réutilisation ultérieure,
- sinon, vous videz l’emplacement le plus ancien et le réutilisez pour héberger vos nouvelles données.
- vous créez une liste fixe d’emplacements (lignes de cache),
- une fois que vous avez quelque chose à mettre en cache, vous allouez un tampon mémoire de taille prédéfinie à l’un de ces emplacements et lui attribuez un identifiant unique. Cela pourrait être un checksum, un hash aléatoire ou même un horodatage, il suffit juste de le cuisiner de la même manière pour obtenir quelque chose d’unique,
- lorsque vous avez besoin des données associées à un identifiant unique, vous interrogez la liste des emplacements et recherchez si cet ID est connu :
- si c’est le cas, vous récupérez son tampon associé,
- si ce n’est pas le cas :
- si vous avez encore des emplacements vides, vous créez le tampon associé et copiez les données pour une réutilisation ultérieure,
- sinon, vous videz l’emplacement le plus ancien et le réutilisez pour héberger vos nouvelles données.
Dans ce processus, vous n’avez besoin de connaître que la taille des tampons et les IDs. C’est très général, vous pouvez mettre en cache n’importe quoi, même des objets différents, votre cache n’a pas à être conscient du contenu, ni même de la manière dont les IDs sont générés. C’est propre, c’est élégant, c’est modeste, c’est générique, je lui confierais ma vie car c’est bien plus robuste que n’importe quel système de sécurité que vous trouvez dans les voitures modernes.
Donc, quand quelque chose d’aussi simple ne fonctionne pas, c’est généralement parce que quelqu’un a essayé quelque chose de “intelligent” et a échoué. Ce que l’équipe de Darktable fait généralement dans ce cas, c’est parcourir toutes les cas particuliers pathologiques et en faire quelque chose d’encore plus compliqué (en traitant toutes les exceptions manuellement avec des heuristiques), juste pour s’assurer que personne plus tard n’ait la chance de trouver la cause profonde de l’erreur.
Par exemple, il y a eu des tentatives de réévaluation de la priorité des lignes de cache pour s’assurer que le module avant celui actuellement édité dans l’interface graphique soit mis en cache. Non seulement cela n’a pas fonctionné, mais cela a renforcé les liens entre le code de la chaîne et le code GUI, d’une manière qui n’était même pas thread-safe (c’est pourquoi cela n’a pas fonctionné). Les éléments GUI devraient se produire à l’entrée et à la sortie des calculs de la chaîne, pas entre les deux, car encore une fois, différents threads, mais cela viole également le principe de modularité (garder les couches de programme séparées et cloisonnées autant que possible), et ce logiciel doit cesser de faire dépendre tout de tout.
Encore une fois, cela m’a pris 8 mois, y compris des pauses obligatoires de ce spectacle d’horreur, pour aller au fond du problème d’une manière qui mène à une solution simplificatrice. Et je vais présenter les résultats de manière linéaire, comme une histoire, mais gardez à l’esprit que j’ai commencé à découvrir des choses de manière floue et aléatoire car tout est éparpillé dans le code source, donc cela semblera moins désordonné qu’il ne l’était vraiment.
Nous commençons avec l’ID unique. Qu’est-ce qui représente vraiment l’état d’un module de manière unique ? Eh bien, un checksum “cryptographique” de ses paramètres internes. Cool, donc Darktable avait cela implémenté depuis longtemps. Sauf qu’il ne tenait pas compte du numéro d’instance du module, et était truffé de toutes sortes de if
dans le processus. Pas complet, pas robuste, même pas nécessaire. Hashez tout, le hash représentera l’état des variables.
Oui, mais les modules peuvent être réordonnés, alors comment prendre en compte l’ordre de la chaîne ? Eh bien, vous prenez tous les hashs de tous les modules, dans l’ordre de la chaîne, et commencez à accumuler linéairement. Excellent. Sauf que Darktable en avait en fait 2 de ceux-là, un pour les besoins GUI qui commençait depuis la fin de la chaîne (donc, dans un ordre inverse), un pour les besoins de la chaîne, dans l’ordre de la chaîne mais inaccessible depuis le GUI (par exemple… pour obtenir un histogramme), et encore une fois, tous deux mélangeant cela avec toutes sortes de vérifications pour gérer des cas particuliers (pipette de couleur, aperçu du masque, etc.).
Sans parler du fait que l’état interne du module ne varie pas que vous soyez dans l’aperçu complet ou dans la miniature de navigation, en chambre noire. Et pourtant, le checksum était entièrement recalculé deux fois, une fois pour chaque chaîne. En réalité, faites cela quatre fois, car il y a aussi le checksum GUI (principalement utilisé pour les modules perspective et retouche)
Et, enfin, lorsque vous êtes zoomé en chambre noire, seule la portion visible de l’image (la Région d’Intérêt, alias ROI) est calculée, ce qui signifie que nous devons garder une trace de notre position dans l’image dans notre mécanisme de cache. Mais cela était complètement omis du calcul du checksum. Gros bug ici, et vieux.
Alors, comment Darktable parvient encore à “fonctionner”, me demandez-vous ?
Eh bien, en vidant plus ou moins complètement le cache sur toute opération pathologique : zoomer, déplacer, prévisualiser un masque, utiliser une pipette de couleur, activer/désactiver les états d’édition des modules crop et perspective. C’est une manière d’assurer la cohérence sans régler la cohérence : brûlez-le. Rendant le tout principalement inutile, comme le montrent les statistiques de frappes du cache (il suffit de lancer ansel -d dev
pour le montrer).
Comment ai-je résolu le problème ?
- Lorsque un nouvel élément d’historique de module est ajouté, le checksum des paramètres est calculée, prenant en compte les paramètres, les masques, les options de fusion, le numéro d’instance, l’ordre dans la chaîne, etc. Cela signifie que toutes les chaînes partagent le même checksum/ID ici (une utilisation future potentielle serait de le stocker dans la base de données),
- Avant qu’une chaîne ne soit calculée, nous calculons le checksum global de tous les modules, de début à la fin, en tenant compte de l’état d’affichage du masque, du checksum des modules précédents, et du ROI (taille et coordonnées). Ce checksum peut être directement accessible par la suite, sans calcul supplémentaire.
- Le cache gère ce checksum global, et seulement cela. Pas de si, pas de mais, pas d’heuristiques, pas de conditions, pas de solutions de contournement.
- Les modules peuvent demander un contournement du cache, par exemple en utilisant une pipette de couleur. Cela contamine les modules ultérieurs dans la chaîne avant que la chaîne ne soit calculée, de sorte que l’état sans cache est connu tôt et n’affecte pas les modules en amont. Cela devrait seulement être une solution de contournement avant que les pipettes de couleur puissent réellement utiliser les lignes de cache directement, et pourrait être réutilisé pour les futurs modules effectuant des choses non standard (peinture ?).
- Lorsque un nouvel élément d’historique de module est ajouté, le checksum des paramètres est calculée, prenant en compte les paramètres, les masques, les options de fusion, le numéro d’instance, l’ordre dans la chaîne, etc. Cela signifie que toutes les chaînes partagent le même checksum/ID ici (une utilisation future potentielle serait de le stocker dans la base de données),
- Avant qu’une chaîne ne soit calculée, nous calculons le checksum global de tous les modules, de début à la fin, en tenant compte de l’état d’affichage du masque, du checksum des modules précédents, et du ROI (taille et coordonnées). Ce checksum peut être directement accessible par la suite, sans calcul supplémentaire.
- Le cache gère ce checksum global, et seulement cela. Pas de si, pas de mais, pas d’heuristiques, pas de conditions, pas de solutions de contournement.
- Les modules peuvent demander un contournement du cache, par exemple en utilisant une pipette de couleur. Cela contamine les modules ultérieurs dans la chaîne avant que la chaîne ne soit calculée, de sorte que l’état sans cache est connu tôt et n’affecte pas les modules en amont. Cela devrait seulement être une solution de contournement avant que les pipettes de couleur puissent réellement utiliser les lignes de cache directement, et pourrait être réutilisé pour les futurs modules effectuant des choses non standard (peinture ?).
- Lorsque un nouvel élément d’historique de module est ajouté, le checksum des paramètres est calculée, prenant en compte les paramètres, les masques, les options de fusion, le numéro d’instance, l’ordre dans la chaîne, etc. Cela signifie que toutes les chaînes partagent le même checksum/ID ici (une utilisation future potentielle serait de le stocker dans la base de données),
- Avant qu’une chaîne ne soit calculée, nous calculons le checksum global de tous les modules, de début à la fin, en tenant compte de l’état d’affichage du masque, du checksum des modules précédents, et du ROI (taille et coordonnées). Ce checksum peut être directement accessible par la suite, sans calcul supplémentaire.
- Le cache gère ce checksum global, et seulement cela. Pas de si, pas de mais, pas d’heuristiques, pas de conditions, pas de solutions de contournement.
- Les modules peuvent demander un contournement du cache, par exemple en utilisant une pipette de couleur. Cela contamine les modules ultérieurs dans la chaîne avant que la chaîne ne soit calculée, de sorte que l’état sans cache est connu tôt et n’affecte pas les modules en amont. Cela devrait seulement être une solution de contournement avant que les pipettes de couleur puissent réellement utiliser les lignes de cache directement, et pourrait être réutilisé pour les futurs modules effectuant des choses non standard (peinture ?).
Bénéfices :
- Le checksum interne, par module, est calculée une fois pour toutes les chaînes,
- Parce que le checksum global, par chaîne, de chaque module est connu avant de commencer la recomputation de la chaîne :
- il peut également être utilisé pour la synchronisation GUI, donc j’ai fusionné les deux checksums Darktable en un,
- il est constant dans le cadre de la chaîne, permettant de partager les lignes de cache entre plusieurs chaînes (par exemple, démosaillage et réduction de bruit) avec des problèmes de verrouillage de threads limités1
- Les modules effectuant des opérations étranges ont une manière uniforme et prévisible de demander un contournement de cache depuis les événements GUI, si jamais ils en avaient besoin.
- Le checksum interne, par module, est calculée une fois pour toutes les chaînes,
- Parce que le checksum global, par chaîne, de chaque module est connu avant de commencer la recomputation de la chaîne :
- il peut également être utilisé pour la synchronisation GUI, donc j’ai fusionné les deux checksums Darktable en un,
- il est constant dans le cadre de la chaîne, permettant de partager les lignes de cache entre plusieurs chaînes (par exemple, démosaillage et réduction de bruit) avec des problèmes de verrouillage de threads limités1
- Les modules effectuant des opérations étranges ont une manière uniforme et prévisible de demander un contournement de cache depuis les événements GUI, si jamais ils en avaient besoin.
- Le checksum interne, par module, est calculée une fois pour toutes les chaînes,
- Parce que le checksum global, par chaîne, de chaque module est connu avant de commencer la recomputation de la chaîne :
- il peut également être utilisé pour la synchronisation GUI, donc j’ai fusionné les deux checksums Darktable en un,
- il est constant dans le cadre de la chaîne, permettant de partager les lignes de cache entre plusieurs chaînes (par exemple, démosaillage et réduction de bruit) avec des problèmes de verrouillage de threads limités1
- Les modules effectuant des opérations étranges ont une manière uniforme et prévisible de demander un contournement de cache depuis les événements GUI, si jamais ils en avaient besoin.
Cette logique est non seulement plus efficace (moins de calculs), elle est aussi plus simple et peut être étendue pour des fonctionnalités intéressantes. Du point de vue du cache, nous ne gérons rien d’autre qu’un checksum, chaque état de module d’intérêt est enroulé dedans.
Mais, plus important encore, le cache est enfin utile, en particulier lors des retours en arrière et des avant-après dans l’historique des modifications, de l’utilisation d’annulations/refaire, ou de l’activation/désactivation des modules. La réactivité globale du GUI est bien meilleure.
Je suis sûr qu’il y a des pièges encore inconnus et des détails que j’ai oubliés de reconfigurer avec la nouvelle logique, et le module retouche est toujours principalement cassé, mais s’adapter à quelque chose d’aussi simple devrait être faisable.
Pendant ce temps dans Darktable 4.8
- Le checksum de la chaîne est calculé pendant l’exécution de la chaîne, il est donc inconnu à l’extérieur,
- À cause de cela, ils n’ont pas dédupliqué les checksums GUI par rapport à ceux de la chaîne… bonne chance pour suivre les incohérences entre les deux à l’avenir,
- Leur code de gestion des caches est plus de deux fois plus large que le mien et utilise des heuristiques (sur le type de chaîne, le type de module, l’état d’affichage des masques, l’utilisation de pipette de couleur et des indices de mise en cache définis manuellement dans les modules) pour contourner les problèmes. Le cache n’est plus indépendant du contenu et bonne chance pour déboguer ces spaghettis.2
- Ils calculent toujours intégralement le checksum des paramètres (internes) des modules deux fois, une pour chaque chaîne,
- Cela leur a pris presque 2 ans pour le faire (depuis la sortie de la version 4.0),
- J’aimerais voir leurs statistiques de frappes/ratés de cache (est-ce que je veux rouvrir ce logiciel et raviver mon PTSD ? Je passe, merci).
- Les gens qui pensent que le fait d’avoir plus de singes agitant leurs mains en l’air garantit une meilleure qualité devraient cesser de penser.
- Le checksum de la chaîne est calculé pendant l’exécution de la chaîne, il est donc inconnu à l’extérieur,
- À cause de cela, ils n’ont pas dédupliqué les checksums GUI par rapport à ceux de la chaîne… bonne chance pour suivre les incohérences entre les deux à l’avenir,
- Leur code de gestion des caches est plus de deux fois plus large que le mien et utilise des heuristiques (sur le type de chaîne, le type de module, l’état d’affichage des masques, l’utilisation de pipette de couleur et des indices de mise en cache définis manuellement dans les modules) pour contourner les problèmes. Le cache n’est plus indépendant du contenu et bonne chance pour déboguer ces spaghettis.2
- Ils calculent toujours intégralement le checksum des paramètres (internes) des modules deux fois, une pour chaque chaîne,
- Cela leur a pris presque 2 ans pour le faire (depuis la sortie de la version 4.0),
- J’aimerais voir leurs statistiques de frappes/ratés de cache (est-ce que je veux rouvrir ce logiciel et raviver mon PTSD ? Je passe, merci).
- Les gens qui pensent que le fait d’avoir plus de singes agitant leurs mains en l’air garantit une meilleure qualité devraient cesser de penser.
- Le checksum de la chaîne est calculé pendant l’exécution de la chaîne, il est donc inconnu à l’extérieur,
- À cause de cela, ils n’ont pas dédupliqué les checksums GUI par rapport à ceux de la chaîne… bonne chance pour suivre les incohérences entre les deux à l’avenir,
- Leur code de gestion des caches est plus de deux fois plus large que le mien et utilise des heuristiques (sur le type de chaîne, le type de module, l’état d’affichage des masques, l’utilisation de pipette de couleur et des indices de mise en cache définis manuellement dans les modules) pour contourner les problèmes. Le cache n’est plus indépendant du contenu et bonne chance pour déboguer ces spaghettis.2
- Ils calculent toujours intégralement le checksum des paramètres (internes) des modules deux fois, une pour chaque chaîne,
- Cela leur a pris presque 2 ans pour le faire (depuis la sortie de la version 4.0),
- J’aimerais voir leurs statistiques de frappes/ratés de cache (est-ce que je veux rouvrir ce logiciel et raviver mon PTSD ? Je passe, merci).
- Les gens qui pensent que le fait d’avoir plus de singes agitant leurs mains en l’air garantit une meilleure qualité devraient cesser de penser.
Conclusion
La quantité de temps passé et de merdes récemment cassées à réparer pour en arriver là était bien insupportable, mais a été aggravé par le code disséminé de manière non modulaire sans distinction claire entre ce qui appartient à l’UI(G), ce qui appartient au backend, ce qui appartient aux histoires de modules et ce qui appartient aux nœuds de la chaîne. La chose cache n’a pris que 8 mois, principalement d’archéologie et d’ingénierie inverse, en plus de ce qui a été déjà fait dans les contrôles GUI et les recalculs explicites de la chaîne.
Il y a encore des problèmes à corriger :
- le nombre de lignes de cache disponibles est une préférence de l’utilisateur et ne vérifie pas la mémoire disponible restante sur l’appareil,
- le module histogrammes/jauges est principalement cassé par conception, car il était géré par des heuristiques spéciales (maintenant supprimées) sur un module invisible dans le GUI (
gamma.c
). La nouvelle logique permet de le forcer-imposer et de récupérer la ligne de cache depuis le fil GUI. - les histogrammes internes des modules ne sont pas immédiatement tracés lorsqu’ils entrent en chambre noire,
- la gestion des pipettes de couleur pourrait être simplifiée et conçue de manière plus élégante,
- la gestion de l’historique comporte encore quelques cas d’angle.
- le nombre de lignes de cache disponibles est une préférence de l’utilisateur et ne vérifie pas la mémoire disponible restante sur l’appareil,
- le module histogrammes/jauges est principalement cassé par conception, car il était géré par des heuristiques spéciales (maintenant supprimées) sur un module invisible dans le GUI (
gamma.c
). La nouvelle logique permet de le forcer-imposer et de récupérer la ligne de cache depuis le fil GUI. - les histogrammes internes des modules ne sont pas immédiatement tracés lorsqu’ils entrent en chambre noire,
- la gestion des pipettes de couleur pourrait être simplifiée et conçue de manière plus élégante,
- la gestion de l’historique comporte encore quelques cas d’angle.
- le nombre de lignes de cache disponibles est une préférence de l’utilisateur et ne vérifie pas la mémoire disponible restante sur l’appareil,
- le module histogrammes/jauges est principalement cassé par conception, car il était géré par des heuristiques spéciales (maintenant supprimées) sur un module invisible dans le GUI (
gamma.c
). La nouvelle logique permet de le forcer-imposer et de récupérer la ligne de cache depuis le fil GUI. - les histogrammes internes des modules ne sont pas immédiatement tracés lorsqu’ils entrent en chambre noire,
- la gestion des pipettes de couleur pourrait être simplifiée et conçue de manière plus élégante,
- la gestion de l’historique comporte encore quelques cas d’angle.
Cependant, puisque je refuse de “réparer” quoi que ce soit si ma réparation ne rend pas les choses plus simples, cette stratégie commence à payer car le code est beaucoup plus linéaire, avec moins de cas à tester, et finalement légèrement plus rapide. À mesure que les progrès avancent, cela devient lentement plus lisible et plus réparable. Ensuite, bien sûr, ébranler le cœur du logiciel à ce point est voué à casser des choses (qui ne devraient pas être cassées si le code était modulaire).
La question légitime se pose donc : pourquoi s’embêter à réparer l’héritage moche d’Ansel/Darktable et ne pas passer à quelque chose de mieux, plus rapide et plus brillant (comme Vkdt) ? Eh bien, Vkdt (ou tout autre nouveau projet) restera un prototype brut, en concurrence avec d’autres prototypes bruts (c’est Open Source en résumé), à des années de distance d’un produit généralement utilisable. Ajouter un autre prototype inachevé/d’à moitié fait au paysage ne servira à rien. Ce serait agréable d’avoir quelque chose de fini pas à moitié et juste assez bien fini, pour changer. En outre, le code (très) ancien de Darktable est propre et solide (eh bien, pour la plupart), ce sont seulement ces dernières années qui ont pris un tournant pour le pire. git blame
montre toujours les mêmes 3 noms sur les lignes vraiment mauvaises, au point où je me retrouve parfois à supprimer automatiquement les lignes correspondantes quand je vois qui les a écrites, par habitude.
Il y a aussi la crainte que, peu importe à quel point Vulkan rend Vkdt rapide, ce qui rend vraiment Darktable mauvais sont de mauvaises décisions, de mauvaises priorités, des erreurs de programmation, des leçons non retenues, et si ces erreurs se reproduisent sur Vkdt, cela pourrait prendre plus de temps pour réaliser les conséquences avec plus de puissance, mais au final les choses iront dans la même direction. Avoir plus de ressources rend plus abordable d’être stupide… jusqu’à ce que ce ne soit plus le cas et que vous réalisiez à quel point vous êtes piégé.
Translated from English by : Aurélien Pierre, ChatGPT. In case of conflict, inconsistency or error, the English version shall prevail.
The source code actually has a 10-years-old
TODO
comment detailing how to do that. ↩︎ ↩︎ ↩︎It should be noted that “my” cache code is actualy pretty much how Roman Lebedev and Johannes Hanika wrote it 10 years ago. I simplified a couple of things, mostly removing stuff added since then, and added nothing of my own, because it’s a Garbage In/Garbage Out situation where you should rather clean your input rather than trying to handle any corner case internally through unlegible heuristics. ↩︎ ↩︎ ↩︎