Maxence Guesdon — SED - INRIA Saclay-Île-de-France
January 30, 2025 — INRIA Saclay-Île-de-France
Gabriel Scherer et Florian Angeletti (Cambium), des mainteneurs du langage OCaml, passent beaucoup de temps à l'organisation de l'activité de développement en plus du travail sur le code (hébergé sur Github). Ils ont besoin de connaître la dynamique de développement, comme la durée moyenne des issues/propositions de changements, nombre de contributeurs récurrents, qui fait de la revue de code et sur quelles parties, quelles parties du code manquent de contributeurs, à qui pourrait-il être proposé d'intégrer l'équipe de mainteneurs, ...
Quelques visualisations et analyses déjà effectuées mais manque de temps pour aller plus loin.
Point important: le tag "Reviewed-by" de github n'est pas utilisé; à la place, le
fichier Changelog
est un peu formatté pour indiquer pour chaque
changement qui l'a proposé, qui l'a implémenté, qui en a fait la revue. Florian Angeletti
a fait un programme pour analyser ce Changelog et le transformer vers le format JSON.
Extrait du fichier Changes d'OCaml:
OCaml 5.2.0 (13 May 2024) ------------------------- (Changes that can break existing programs are marked with a "*") ### Restored and new backends: - #12276, #12601: native-code compilation for POWER (64 bits, little-endian) (Xavier Leroy, review by KC Sivaramakrishnan, Anil Madhavapeddy, and Stephen Dolan) - #12667: extend the latter to POWER 64 bits, big-endian, ELFv2 ABI (A. Wilcox, review by Xavier Leroy) ### Language features: - #12295, #12568: Give `while true' a polymorphic type, similarly to `assert false' (Jeremy Yallop, review by Nicolás Ojeda Bär and Gabriel Scherer, suggestion by Rodolphe Lepigre and John Whitington) - #12315: Use type annotations from arguments in let rec (Stephen Dolan, review by Gabriel Scherer) - #11252, RFC 27: Support raw identifier syntax \#foo (Stephen Dolan, review by David Allsopp, Gabriel Scherer and Olivier Nicole)
Le développement d'OCaml est (malheureusement) sur Github ➩ utilisation de l'API Github (JSON via HTTP) pour récupérer les données des issues, PR, commits, commentaires, ...
Requêtes HTTP sur des URL de la forme
https://api.github.com/repos/<PROPRIÉTAIRE>/<DÉPOT>/...
Exemple:
https://api.github.com/repos/ocaml/ocaml/issues
Dans l'en-tête:
...
Accept: application/vnd.github+json
Authorization: Bearer <token>
X-GitHub-Api-Version: 2022-11-28
Limite imposée par Github de 5000 requêtes par heure (donc environ 4 requêtes en 3 secondes). Une requête pour lister 100 commits, 100 issues ou 100 PR. Une requête par commit, puis une requête pour lister 100 commentaires d'un commit, puis une requête par commentaire. Idem pour les issues et les PR, avec en plus leurs événements (fermeture, ....). Avec plus de 31700 commits et 13500 issues/PRs, on arrive à plusieurs dizaines de milliers de requêtes, donc plusieurs heures pour tout récupérer.
Il est évidemment inenvisageable de tout télécharger à chaque lancement. Une approche par cache ne fonctionne pas complètement: par exemple un issue ou un commentaire peut avoir été modifié et si on cache la requête qui le récupère on rate cette modification.
Solution: la récupération incrémentale.
Solution envisagée: lister les événements du dépôt depuis la dernière fois (par la date) qu'on a récupéré les données. Malheureusement, Github ne permet de récupérer qu'un nombre limité des derniers événements d'un dépôt.
Solution:
Tout est stocké dans des fichiers JSON (1 par commit avec ses éléments attachés, idem pour chaque issue/PR et contributeur) ➩ + de 550Mo.
Des petites choses auxquelles on ne pense pas forcément de but en blanc:
➩ ajout de la possibilité de lister des contributeurs explicitement dans un fichier JSON, avec leurs différents noms et adresses mails, pour les raccrocher à un seul login. Ça a donné parfois lieu à un peu de recherches pour trouver qui se cachait derrière un login ou une adresse mail dans un commit.
Schéma partiel des données récupérées via l'API JSON:
Il existe un paquet OCaml pour faire des requêtes auprès de Github mais:
J'ai préféré utiliser une de mes bibliothèques
(OCaml-Ldp) qui contient
un module (
let mk_http conf = let policy_ref, cache = let (policy_ref, cache) = Cache.mk_cache conf.Conf.cache_dir in let module C = Ldp.Http.Make_cache (val cache) in policy_ref, (module C:Ldp.Http.Cache) in let%lwt h = Ldp_tls.make ~cache_impl:cache ~dbg:(fun s -> Statocaml.Log.debug (fun m -> m "%s" s); Lwt.return_unit) () in let module H = struct let conf = conf let user = conf.user let repo = conf.repo let cache_policy_ref = policy_ref module HJ = Ldp.Http.Http_ct (val h) (struct type t = Yojson.Safe.t let ct = Ldp.Types.content_type_of_string "application/json" let to_string j = Yojson.Safe.to_string j let of_string = function | "" -> `Assoc [] | s -> Yojson.Safe.from_string s end) include HJ end in Lwt.return (module H : Http_t)
De plus, une autre bibliothèque (Ocf) me permet de définir des structures de données pour faire lire et écrire des fichiers de configuration en JSON. En définissant des structures de données avec les mêmes champs que les réponses de Github, j'avais d'un coup ce qu'il faut pour lire les réponses de Github et les stocker en local, modulo une astuce pour accrocher les éléments attachés (commentaires, événements, ...) et qui nécessitent des requêtes supplémentaires.
type issue = { assignees : user list [@ocf W.list user_wrapper, []] ; body : string option [@ocf W.option W.string, None] ; closed_at : Ptime.t option [@ocf W.option ptime_wrapper, None] ; closed_by : user option [@ocf W.option user_wrapper, None] ; comments : comment list [@ocf W.list comment_wrapper, []] [@ocf.label "ignore_comments"] ; comments_url : Iri.t [@ocf iri_wrapper, Iri.of_string ""] ; ... user : user [@ocf user_wrapper, default_user] ; duration : Ptime.span option [@ocf W.(option span_wrapper), None][@ocf.label "ignore_duration"]; ... } [@@ocf] (* génère un wrapper pour lire et écrire cette structure en JSON *)
Et la fonction de récupération des issues:
let issues http ?since ?limit () = let iri = let iri = issues_iri (http_user_repo http) in match since with | None -> iri | Some date -> iri_add_since_param iri date in let%lwt l = get_paged http ?limit iri Types.issue_wrapper in let%lwt l = Lwt_list.map_s (issue_with_comments http) l in let%lwt l = Lwt_list.map_s (issue_with_events http) l in let%lwt l = Lwt_list.map_s (issue_with_timeline http) l in let%lwt l = Lwt_list.map_s (issue_with_pull_request http) l in let l = List.map issue_with_duration l in Lwt.return l
On peut distinguer plusieurs sous-systèmes dans OCaml: typeur, parseur, bibliothèque standard, documentation, ...
Il est intéressant de pouvoir indiquer, pour chaque commit, issue et PR, le(s) sous-système(s) concerné(s), ce qui permet aussi de savoir sur quels systèmes intervient un contributeur par exemple.
Le développement d'OCaml n'utilise pas les tags de manière systématique pour indiquer les sous-systèmes concernés par un issue/PR.
Les sous-systèmes sont donc décrits dans un fichier JSON séparé, utilisant des expressions régulières pour associer un fichier donné à une liste de sous-systèmes (en pratique souvent un seul sous-système), celle associée à la première expression qui attrape le nom du fichier. On attribue à un issue/PR les sous-systèmes correspondants aux fichiers modifiés par les commits associés.
Extrait des définitions des sous-systèmes pour OCaml:
{ "subsystems": { "ty": { label: "Typing", color: "#ee6363" }, "pars": { label: "Parsing", color: "#eedd82" }, "std": { label: "Stdlib", color: "#20b2aa" }, "cg": { label: "Code generation", color: "#90ee90" }, "olibs": { label: "Otherlibs", color: "#969696" }, "test": { label: "Testing", color: "#cd853f" }, "comp": { label: "Compiler", color: "#dda0dd" }, "tools": { label: "Tools", color: "#00f5ff" }, "p4": { label: "Camlp4", color: "#8ee5ee" }, "obld": { label: "Ocamlbuild", color: "#8b008b" }, "doc": { label: "Documentation", color: "#adff2f" }, "bld": { label: "Build", color: "#800080" } }, "rules": [ { re_path: [ ".*\\.gif", ".*\\.tif", ".*\\.png", ".*_camltex\\.tex", "\\.github/.*", "\\.git.*", "\\.ignore", "\\.ocp-indent", "\\.depend.*", "\\.travis.*", "\\.cvsignore", "merlin", "\\.mailmap", "ocaml-variants\\.install" ], ids: [] }, { re_path: ["Makefile.*", ".*/Makefile.*", "aclocal\\.m4", "configure\\.ac", "appveyor.*", "myocamlbuild\\.ml", "myocamlbuild_config\\.mli", "config/.*", "dune", "dune-project", "opam", "ocaml\\.opam", "cross-ios-build\\.sh" ], ids: ["bld"] }, ...
La couleur d'un sous-système est utilisée dans certaines visualisations.
Au final, nous avons les données suivantes en entrée:
Création d'une structure de données unique par contributeur (croisement sur login, mail, nom, ...).
Remplacement de l'identifiant de user
Github par l'identifiant de contributeur
dans toutes les structures de données.
Définition de périodes à partir de toutes les dates:
Pour chaque contributeur et pour chaque période, on stocke
Agrégation au niveau global, par période également
Possibilité de définir des groupes de contributeurs en entrée ("Core team", "Recherche française", entreprise X ou Y, ...).
Agrégation des statistiques des contributeurs dans ces groupes et utilisation de ces groupes dans certaines visualisations.
Demande beaucoup de travail pour définir les groupes et l'appartenance des membres avec pour chacun leur date d'entrée et de sortie d'un groupe.
➩ pas fait très précisément.
J'ai choisi de générer non pas seulement des fichiers de visualisation, mais tout un site web permettant d'accéder aux visualisations, ainsi qu'au profil de chaque contributeur, avec des visualisations (répartition des commits selon les sous-systèmes, ...) et des chiffres bruts.
Cela permet de détecter éventuellement des aberrations dues à des bogues, en parcourant des profils dont on a une idée des contributions.
L'outil génère des visualisations prédéfinies et est réutilisable pour d'autres projets.
Cependant, on peut souhaiter avoir certaines visualisations avec des paramètres différents: période, périodicité, affichage de certaines informations ou non, ...
L'outil permet d'indiquer dans un fichier des visualisations supplémentaires à générer, avec leurs paramètres. Mais plutôt que passer du temps dans une boucle consistant à changer des paramètres dans le fichier et relancer la génération, une interface graphique permet de tester plus rapidement ces paramètres:
Ensuite, un bouton copie la description de la visualisation dans le presse-papier:
{ "w": 1358, "h": 922, "outfile": "/tmp/ab61a7.png", "terminal": "pngcairo", "title": "PR cohorts (open between 2023/01/01 and 2025/01/09)", "plotter": "pr_cohorts", "cohort_duration": 15, "before": (2025, 1, 9), "after": (2023, 1, 1) }
Il ne reste plus qu'à la coller dans le fichier des visualisations supplémentaires passé à l'outil de génération du site (on peut aussi changer le format de sortie pour utiliser du SVG).
L'interface graphique permet également de générer directement une visualisation dans un fichier.
Outils:
Bibliothèques:
Détection de communautés.
D'autres visualisations.
Application à d'autres logiciels, voire plusieurs logiciels en même temps pour détecter des communautés ?
Distribution du code des outils: pas très chaud car c'est encore contribuer à (et donc valoriser) Github (Microsoft) indirectement.
Possibilité de récupérer et utiliser les données de plateformes type Gitlab ? Cela suppose d'avoir une structure de données indépendante de Github et de gérer partout le possible manque de données ou leurs sémantiques différentes.
Slideshow scripts de Dave Raggett.
Jolis dessins réalisés sur Excalidraw.
Génération du XHTML avec Stog.