Logo Yassir

CubeFlow

Un moteur de cube OLAP écrit en Rust

17 sept. 2026 - 7 minute read
feature image

Je travaille sur des plateformes de performance financière depuis six ans. Essbase, HFM, Planning, OneStream : des moteurs multidimensionnels que l’on configure, que l’on interroge, que l’on optimise, et dont on finit par connaître les comportements par cœur sans jamais voir ce qu’il y a dedans. En avril 2024, j’ai voulu savoir. Pas lire un article sur le sujet : écrire le moteur.

CubeFlow est né de là. C’est un moteur de cube OLAP multidimensionnel en Rust, dans la lignée d’Essbase et de TM1. Deux ans et demi plus tard, il compile, il calcule, il persiste, il se relève après un crash, et une interface Excel sait l’interroger. Cet article est un point d’étape.

Le modèle, d’abord

Un moteur multidimensionnel repose sur très peu de concepts, et tout le reste en découle.

  • Cube : le conteneur de données, composé d’une Outline et d’un DataCube.
  • Outline : la liste ordonnée des dimensions.
  • Dimension : un axe hiérarchique (Temps, Compte, Entité) avec un membre racine.
  • Membre : un nœud d’arbre avec un identifiant, un parent, un ordre de fratrie et des propriétés.
  • POV, point of view : un membre par dimension, ce qui identifie une cellule.

Ce dernier point est le plus structurant. Un POV se sérialise de façon canonique, et le chemin chaud en f64 en dérive directement une clé binaire de cellule. Tout l’enjeu de performance d’un moteur OLAP tient dans cette traduction : passer d’une coordonnée métier lisible à une adresse mémoire, des millions de fois par seconde, sans allouer.

L’architecture

Le moteur se découpe en cinq sous-modules, chacun avec une responsabilité que je me suis interdit de mélanger.

Le calcul. Un cube compilé en structures orientées colonnes (SoA, CSR), un noyau de requête vectorisé AVX2, un noyau de rebuild, la résolution de POV, les requêtes en grille, un planificateur de requêtes, des vues matérialisées, et une agrégation accélérée par GPU via wgpu.

Le stockage. Une image dense des feuilles maintenue en mémoire dans exactement la disposition sur laquelle les noyaux calculent, persistée en fichiers découpés en chunks avec une disposition auto-descriptive. Charger, c’est lire les étendues peuplées. Écrire, c’est un seul stockage en place. La durabilité repose sur un WAL et un checkpoint par chunks. Les cellules entières et textuelles vivent dans LMDB, les métadonnées structurelles et les ACL dans SQLite.

Le runtime. Un coordinateur, des pools de threads conscients du SIMD et du NUMA, un comptable mémoire, un rebuild automatique, un suivi des zones sales, et une concurrence événementielle sur canaux MPSC avec réponses en oneshot.

Le cache. Un cache de membres sur DashMap, et un cache de résultats d’agrégation protégé par époques.

Les erreurs. Un type d’erreur unifié, avec des variantes par domaine (crypto, configuration, stockage, authentification externe, calcul) et un code numérique stable par variante, propagé jusqu’aux réponses REST. Un client SDK peut réagir à 3001 sans parser un message.

Ce qui rend un projet solo maintenable

C’est la partie dont je suis le plus satisfait, et elle n’a rien à voir avec l’algorithmique.

  • Métriques Prometheus par cube, publiées à zéro dès l’ouverture du cube, pour qu’une première anomalie soit une augmentation visible et non le premier point d’une série qui vient de naître.
  • Spécification OpenAPI générée, et un contrôle de dérive de contrat en CI entre le serveur et le client TypeScript.
  • SemVer et un changelog au format Keep a Changelog, où chaque entrée explique la décision et pas seulement le diff.
  • Trois workflows : intégration, release, et un soak nocturne qui fait tourner le moteur longtemps pour débusquer ce qu’un test de dix secondes ne verra jamais.

Un projet personnel meurt quand il devient impossible d’y revenir après trois semaines d’absence. Ces quatre points sont ce qui me permet de reprendre le fil en dix minutes.

Septembre : rendre le banc de mesure digne de confiance

Ce mois-ci, j’ai travaillé l’allocateur mémoire et le chemin de rebuild : étude comparative d’allocateurs, mimalloc retenu et unifié entre le démon et les benchs, tampons pré-dimensionnés, un seul tri non-feuille par rebuild incrémental, tableaux de travail mis en pool et réinitialisés au strict nécessaire, index de survol partitionné par shard pour supprimer les allocations par lot.

Avant d’accepter le moindre gain, j’ai audité la chaîne de mesure elle-même. Bien m’en a pris : le processus de bench héritait d’un drapeau PR_SET_THP_DISABLE de son environnement d’exécution, et travaillait donc sur des pages de 4 Kio, là où le démon tourne avec les transparent huge pages. Deux programmes, deux régimes mémoire, et donc une comparaison faussée à la racine.

Ce genre d’écart ne se voit pas dans un profil de flamme ni dans un écart type : le banc est parfaitement reproductible, il reproduit simplement autre chose que la production. Le repérer demande de comparer l’état réel du processus mesuré à celui du processus servi.

Le harnais force désormais cet état explicitement, et j’ai réenregistré l’ensemble des références sur quatre familles de benchmarks, en consignant le vol de CPU de chaque exécution pour que les chiffres restent comparables entre deux machines.

Un chiffre de performance sans son environnement de mesure n’est pas une donnée, c’est une impression.

C’est vrai d’un moteur OLAP. C’est tout aussi vrai des promesses de gain que l’on entend aujourd’hui sur l’IA : la première question à poser n’est pas « combien », c’est « mesuré comment, et contre quoi ».

CubeFlow UI : le moteur ne suffit pas

Un moteur sans client ne prouve rien. CubeFlow UI est un workspace Bun en TypeScript, avec trois paquets et une règle simple : le contrat serveur est partagé, jamais recopié.

PaquetRôle
@cubeflow/clientclient REST typé et modèle de grille, sans DOM, sans Office.js
@cubeflow/excelvolet Excel : requête dans une plage, écriture en retour, bacs à sable de simulation
@cubeflow/admininterface d’administration React et Vite : Cubes, Modeler, Explorer, Sécurité, Opérations

L’admin est embarquée dans le binaire du démon au moment de la release : un seul artefact à déployer. Chaque client reçoit l’URL du serveur explicitement et porte son jeton lui-même, sans hypothèse de même origine ni cookie, pour que le même code tourne dans le volet Excel, dans l’admin embarquée et dans une coque desktop.

La CI vérifie la dérive de contrat entre le client publié et le document du serveur : bloquante contre la branche principale, informative contre la release épinglée. Un changement de contrat serveur, c’est un commit qui traverse le client et les applications.

Les ajouts récents vont tous dans la même direction, rendre le moteur explicable :

  • expliquer une cellule sélectionnée dans l’Explorer, c’est-à-dire montrer d’où vient un chiffre
  • voir en tant qu’un autre principal, pour vérifier la sécurité telle que l’utilisateur la vit
  • les événements serveur en direct dans l’écran Opérations

Quiconque a déjà passé une journée à expliquer à un contrôleur de gestion pourquoi une cellule affiche ce qu’elle affiche comprendra pourquoi ces trois fonctionnalités sont arrivées ensemble.

Pourquoi je continue

CubeFlow n’est pas un produit, et je n’ai aucune intention d’aller concurrencer un éditeur. C’est un banc d’essai, et c’est le meilleur investissement technique que j’aie fait.

Comprendre pourquoi un cube est lent ne s’apprend pas dans une documentation éditeur. Ça s’apprend en écrivant le noyau qui balaie les cellules, en se trompant sur la disposition mémoire, en découvrant que le vrai coût était dans une allocation par lot que personne ne soupçonnait. Depuis, je lis les comportements d’Essbase et de OneStream différemment : je sais ce qui se passe en dessous, et ce que la contrainte physique impose.

Deux ans et demi, 456 commits, et il reste largement de quoi faire.

Références