Statocaml

Maxence Guesdon — SED - INRIA Saclay-Île-de-France

January 30, 2025 — INRIA Saclay-Île-de-France

Plan

Contexte

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)

But

Approche, méthode

  1. Architecture a priori simple: lire des données, faire des calculs, générer des graphes et pages web.
  2. Dégrossir:
    • À quelles informations a-t-on accès ? comment ? sous quelle forme ?
    • Est-ce qu'on peut récupérer toutes les informations dont on a besoin, ou dont on sait qu'on aura besoin ?
    • Avec quels outils va-t-on générer les graphes ?
    • Voir «Comment ça se présente ?»
  3. Implémenter un premier flux:
    • récupération des informations, stockage, relecture,
    • génération des visualisations qui, de façon intuitive, pourraient poser le plus de soucis au niveau de l'information disponible.
    • ➩meilleure appréhension des points délicats, des motifs récurrents, ...
  4. Ré-usinage, par cycles, au fur et à mesure des fonctionnalités à ajouter: ajout d'une structure de profil unique par contributeur, périodes, ...

Les données

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

Récupération des données

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.

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.

Remarques

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.

Données Github

Schéma partiel des données récupérées via l'API JSON:

user [id]full_user [id]comment [id]review_comment [id]event [id]git_commitcommit [url]release [id]pull_requestissue [number]user +- name- email- html_url- ...- login- url- user- body- created_at- updated_at- in_reply_to- user- body- created_at- updated_at- in_reply_to- start_line- line- ...- actor, author, user, committer assignee, assigner, ...- commit_url- created_at- updated_at- (kind of) event- body- ...git_user_date- name- email- date- author- committer- message- urlgit_commit_file- filename- status- additions- deletions- sha- comments_url- author- committer- files- commit- parents- url- review_comments_url- commits_url- merged_at- body- assignees, user- created_at- closed_at- comments_url- id- title- state- timeline- events_url- pull_request- ...- tag_name- name- bodycreated_at- ...Données incluses dans la réponse à une requêteNécessité de faire une requête pour accéder aux données(ou plusieurs si c'est une liste)

Concrètement (requêtes JSON via HTTP)

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 (Http) permettant de faire des requêtes HTTP avec une politique de cache en paramètre (via un foncteur). De plus, il est possible également de passer un module pour lire des réponses dans un certain format, ici JSON, plutôt qu'en texte brut.

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)

Concrètement (types, récupération, stockage)

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

Sous-systèmes

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.

Données en entrée

Au final, nous avons les données suivantes en entrée:

ChangelogContribu-teurs pré-définisSous-sytèmesgithub.comDonnées GithublocalesincrémentalinitialStatocaml/goStatocaml/fetchAggrégation etcroisementdes donnéesconf.json- dépôt- utilisateur- token- répertoires (données, cache)- date de dernier fetchoptionnels

Agrégation et croisement

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.

Périodes

Définition de périodes à partir de toutes les dates:

Des nombres et des ensembles

Pour chaque contributeur et pour chaque période, on stocke:

Agrégation au niveau global, par période également:

Groupes

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.

Visualisations

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.

➩Visite du site

Visualisations sur mesure

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.

Architecture finale

ChangelogContribu-teurs pré-définisVisualisa-tions ad-ditionnellesSous-sytèmesgithub.comDonnées GithublocalesincrémentalinitialStatocaml/goStatocaml/fetchAggrégationet croisementdes donnéesconf.json- dépôt- utilisateur- token- répertoires (données, cache)- date de dernier fetchoptionnelsSite web avecvisualisations etrésultats Groupes

Bibliothèques et outils utilisées

Outils:

Bibliothèques:

A suivre...

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.

Merci

Questions ?

Crédits

Slideshow scripts de Dave Raggett.

Jolis dessins réalisés sur Excalidraw.

Génération du XHTML avec Stog.