Overleaf-Benchmark.pdf
Résumé
Les déploiements Overleaf auto-hébergés sont généralement dimensionnés selon une seule règle empirique : un cœur CPU et un gigaoctet de mémoire pour cinq à dix utilisateurs simultanés. Nous montrons que cette règle n’est pas seulement imprécise mais structurellement fausse, car elle suppose qu’une seule dimension de ressource gouverne la capacité alors qu’en réalité deux murs indépendants le font, et parce que deux paramètres logiciels — dont aucun ne relève du matériel — dominent le résultat d’un facteur pouvant aller jusqu’à quatre. Nous mesurons un déploiement Ayakaleaf Pro v6.2.2 standard avec compilation en sandbox (TeX Live 2025) sur 21 configurations CPU/mémoire, dans des invités QEMU/KVM dont les cœurs hôtes sont verrouillés à 3,0 GHz. La charge de travail est une véritable thèse XeLaTeX de 63 pages compilée simultanément par jusqu’à plusieurs centaines de comptes utilisateurs distincts. Nous constatons qu’en dessous de 32 Gio de mémoire invitée, le nombre de cœurs est presque sans importance — à 16 Gio, la capacité mesurée des invités à 4, 8 et 16 vCPU diffère de moins de 8 % — et que la capacité est au contraire gouvernée par un mur mémoire super-linéaire découlant du cache de pages partagé sur l’arborescence TeX Live. Pour savoir si ces lois résistent à un changement d’échelle d’un ordre de grandeur, nous répétons le balayage sur un unique serveur de 64 cœurs et 995 Gio. Il soutient 1024 compilations à froid simultanées avec 100 % de réussite — huit fois son nombre de threads — et nous n’atteignons jamais son plafond. Le chiffre utile n’est pas ce plafond mais le coude situé en dessous : la latence de queue augmente de 20 à 40 % à chaque doublement jusqu’à , puis de 190 % à . Une capacité définie comme « la plus grande concurrence qui n’échoue pas » surestimerait donc le point de fonctionnement utilisable d’un facteur quatre. Sur cette machine, la mémoire n’est jamais la ressource limitante ; la limite est le CPU, conjugué au rythme auquel le démon de conteneurs peut admettre de nouvelles sandboxes, qui sature vers 200 quel que soit le nombre de compilations demandées. Nous identifions en outre deux effets liés à l’implémentation, invisibles pour la planification de capacité. Premièrement, CLSI impose un plafond codé en dur de 65 compilations simultanées, qui n’est exposé par aucune variable d’environnement ; au-delà, les utilisateurs reçoivent immédiatement une erreur HTTP 503 au lieu d’être mis en file d’attente. Deuxièmement, la limite de mémoire par conteneur du runner Docker est inopérante depuis son introduction en 2018, tant par sa valeur que par son emplacement, de sorte qu’un événement de manque de mémoire fait tomber l’hôte entier plutôt qu’une seule compilation. Lever le plafond de concurrence et faire passer le délai de compilation par défaut de 180 s à 300 s augmente la capacité mesurée d’un invité 8 vCPU / 48 Gio de 64 à 268 compilations simultanées — un facteur de 4,2 sans aucun coût matériel. Enfin, nous montrons que la concurrence dans ce système n’apporte rien d’autre que du temps partagé, et que la charge de travail n’est limitée que par la fréquence d’horloge. Une loi de dégradation ajustée donne , proche d’un ralentissement parfaitement proportionnel, et un balayage de fréquence sur toute la plage de 1,0 à 5,5 GHz de la machine ramène trente mesures sur avec et une dispersion résiduelle de 5,1 %. Une horloge 5,5× plus rapide apporte une accélération de 5,5× sans rendement décroissant ; c’est en ce sens que fréquence et cœurs n’achètent pas la même chose : la fréquence accélère la compilation de chaque utilisateur, les cœurs ne font qu’admettre davantage d’utilisateurs.1. Introduction
Overleaf est l’éditeur LaTeX collaboratif dominant, et sa distribution sur site est largement déployée par les universités et les groupes de recherche qui ne peuvent pas envoyer des manuscrits non publiés vers un cloud tiers. Le dimensionnement d’un tel déploiement est une question pratique récurrente : pour un budget matériel donné, combien de personnes peuvent réellement appuyer sur « Recompile » en même temps ? La recommandation officielle est une règle linéaire — environ un cœur et un gigaoctet pour cinq à dix utilisateurs simultanés — qui présuppose que la capacité évolue de manière régulière et conjointe dans les deux ressources. Nos mesures la contredisent de trois manières.1.1 La capacité est gouvernée par deux murs indépendants, et non un seul
Une configuration échoue soit parce que la mémoire est épuisée, auquel cas la pile Overleaf elle-même meurt et renvoie HTTP 502, soit parce que les compilations dépassent le délai côté serveur, auquel cas CLSI signaletimedout alors que des gigaoctets de mémoire restent inutilisés. Ces deux régimes ont des comportements d’évolution totalement différents et des remèdes différents. Ajouter des cœurs à une configuration limitée par la mémoire n’est pas seulement inefficace, c’est parfois contre-productif : nous mesurons des configurations où l’augmentation du nombre de cœurs réduit la capacité, car davantage de cœurs font progresser les compilations concurrentes en parfaite synchronie, si bien que leurs pics de demande mémoire coïncident au lieu de s’entrelacer.
1.2 Les paramètres logiciels dominent le matériel
Le délai de compilation est un champ par utilisateur dans MongoDB dont la valeur par défaut de 180 s plafonne silencieusement les configurations limitées par le CPU. Le porter à 300 s multiplie la capacité mesurée jusqu’à 4,2 sur un matériel inchangé. Indépendamment, CLSI refuse plus de 65 compilations simultanées en raison d’une constante codée en dur. Toute étude de capacité — et tout déploiement — qui ne tient pas compte de ces deux éléments mesure le logiciel, pas la machine.1.3 La concurrence est du temps partagé, pas du parallélisme
Comme une compilation LaTeX est monothread, servir utilisateurs simultanés sur cœurs ne fait pas terminer le système plus tôt ; cela fait attendre chaque utilisateur proportionnellement plus longtemps. La question « combien d’utilisateurs simultanés sont pris en charge » est donc mal posée tant que l’on n’a pas fixé combien de temps un utilisateur est prêt à attendre. Nous rendons cette dépendance explicite et la quantifions.1.4 Contributions
- Une matrice de capacité sur 21 configurations CPU/mémoire mesurées dans des conditions d’horloge verrouillée et vérifiées par répétition, avec la contrainte limitante identifiée pour chaque configuration à partir de sa signature d’échec.
- Deux modèles ajustés : un modèle de capacité séparant un mur mémoire super-linéaire d’un plafond CPU, et un modèle de latence établissant un comportement de pur temps partagé.
- L’identification et la confirmation expérimentale de deux problèmes d’implémentation dans le système déployé, dont une limite de mémoire de conteneur inopérante depuis 2018.
- Une quantification du compromis entre délai de compilation et capacité, qui, selon nous, doit être indiqué avec tout chiffre de concurrence.
2. Contexte
2.1 Chemin de compilation
Une requête de compilation Overleaf transite parweb clsi un conteneur de compilation. Dans un déploiement à compilations en sandbox (SIBLING_CONTAINERS_ENABLED=true), CLSI n’exécute pas latexmk dans son propre processus ; il demande au démon Docker de l’hôte, accessible via un socket monté, de démarrer un nouveau conteneur à partir d’une image TeX Live avec le répertoire du projet monté sur /compile. Une compilation correspond donc à un conteneur éphémère exécutant un processus latexmk.
Trois conséquences en découlent, et toutes trois façonnent les mesures de cet article. Premièrement, l’unité de travail est un processus monothread : XeLaTeX ne se parallélise pas. Deuxièmement, l’isolation des ressources par compilation correspond à ce que demande le runner Docker — nous montrons au §6.2 qu’il ne demande en pratique rien. Troisièmement, l’ensemble de travail est dominé non pas par le document mais par l’arborescence TeX Live, un corpus en lecture seule d’environ 32 Gio que chaque compilation concurrente lit et partage donc via le cache de pages de l’hôte. Ce partage est à l’origine de l’évolution super-linéaire de la mémoire que nous observons.
2.2 Activer les compilations en sandbox
L’édition communautaire d’Overleaf exécutelatexmk dans le conteneur applicatif lui-même. Ayakaleaf Pro, comme Overleaf Server Pro, peut à la place exécuter chaque compilation dans un conteneur frère — un conteneur démarré par l’application sur le démon Docker de l’hôte plutôt qu’imbriqué dans le conteneur applicatif. Deux paramètres du Toolkit activent ce mode :
SANDBOXED_COMPILES=true, SANDBOXED_COMPILES_SIBLING_CONTAINERS=true et SANDBOXED_COMPILES_HOST_DIR, cette dernière étant le chemin hôte du répertoire de compilation. Ce chemin est important : comme le démon qui démarre le conteneur de compilation est celui de l’hôte, le point de montage qui lui est fourni doit pouvoir être résolu dans l’espace de noms de l’hôte, et non dans celui du conteneur applicatif. Le config/env.sh de Server Pro force en outre TEXLIVE_IMAGE_USER=www-data dans ce mode afin que les fichiers écrits par le conteneur de compilation aient un propriétaire cohérent.
La vérification est directe : pendant une compilation, l’hôte affiche un conteneur nommé project-{projectId}-{userId}-{hash} exécutant latexmk depuis l’image TeX Live, qui se termine avec le code 0. C’est l’unité dont nous mesurons la multiplicité tout au long de l’article, et dont nous signalons l’absence totale de limites de ressources au §6.2.
Les conteneurs frères rendent la mesure propre — chaque compilation est une entité du système d’exploitation observable et ordonnancée indépendamment — mais ils signifient aussi que c’est le noyau de l’invité, et non Overleaf, qui arbitre le CPU et la mémoire entre les compilations. Chaque loi d’évolution de cet article est donc une propriété de l’ordonnanceur Linux appliqué à processus monothread, ce qui explique sa grande régularité.

clsi. Aucun des deux ne domine — le coût de compilation d’un projet est déterminé par l’arborescence TeX Live de 32 Gio que chaque compilation concurrente lit via le cache de pages partagé.

git-bridge unique. La configuration par défaut du Toolkit (b), celle que nous mesurons, a un multiplicateur de un : une constante dimensionnée pour un membre d’une flotte devient ainsi le plafond de toute l’installation.

clsi-cache. Un projet est affecté par , c’est-à-dire que l’espace de hachage est découpé en autant de secteurs égaux qu’il y a de shards. Il s’agit d’un hachage modulo, et non d’un hachage cohérent en anneau : faire passer la flotte de trois à quatre shards repartitionne tout l’espace et réaffecte pratiquement chaque projet (a, b). C’est précisément pourquoi l’implémentation a besoin d’une rampe explicite de re-sharding en ligne, déplaçant une fraction croissant linéairement des projets de currentShards vers desiredShards sur une fenêtre de temps, plutôt que le mouvement de qu’offrirait un anneau de hachage cohérent. Lorsque le disjoncteur d’un shard est déclenché, le sel est incrémenté et le shard retiré de la liste des candidats, de sorte que la recherche continue de sonder au lieu d’échouer (c).
2.3 Les deux modes de défaillance
Chaque configuration mesurée échoue d’exactement l’une de deux manières, et la distinction est visible dans le statut de la réponse plutôt que déduite :- Épuisement de la mémoire — la pile Overleaf elle-même cesse de répondre et la requête renvoie HTTP 502. La mémoire disponible de l’invité au niveau d’échec est généralement inférieure à 500 Mio.
- Dépassement du délai de compilation — CLSI interrompt la compilation au délai défini par utilisateur et signale le statut
timedout. La mémoire disponible au niveau d’échec atteint souvent plusieurs gigaoctets.
3. Méthodologie
3.1 Banc d’essai et contrôle de l’horloge
Tous les invités tournent sous QEMU/KVM sur un unique hôte Intel Core i9-14900K doté de 62 Gio de RAM et d’un stockage NVMe. L’invité est Ubuntu 24.04 avec Docker 29.7 et l’Overleaf Toolkit déployant Ayakaleaf Pro v6.2.2 avec compilations en sandbox surtexlive-full:2025.1.
Un CPU de bureau grand public est un piètre substitut à un serveur si sa fréquence n’est pas contrôlée. KVM n’offre aucun mécanisme pour définir une horloge virtuelle : un vCPU est un thread de l’hôte et tourne à la fréquence du cœur hôte, quelle qu’elle soit. Nous contraignons donc directement l’hôte, en désactivant le turbo et en fixant scaling_max_freq à 3,0 GHz sur chaque cœur, et nous épinglons les vCPU de l’invité sur des P-cores physiques avec taskset. La distinction compte sur un CPU à cœurs hybrides : les E-cores de cette puce ont une fréquence de base de 2,4 GHz et ne peuvent pas atteindre 3,0 GHz une fois le turbo désactivé, de sorte qu’une exécution qui s’y égare mesure silencieusement une machine plus lente. Sous pleine charge, nous vérifions exactement 3000 MHz sur les seize threads épinglés. Un script de garde vérifie cet invariant avant chaque benchmark et refuse de démarrer sinon ; il a détecté une réinitialisation silencieuse du gouverneur pendant l’étude.
3.2 Un second banc d’essai : un grand runner unique
La matrice QEMU isole une variable à la fois, mais elle plafonne à seize threads épinglés. Pour savoir si les mêmes lois tiennent encore un ordre de grandeur plus haut, nous avons répété le balayage de concurrence sur un unique grand serveur : un AMD EPYC 7773X (Milan-X, 64 cœurs / 128 threads, 768 Mio de L3) avec 995 Gio de RAM, exécutant la même image Ayakaleaf Pro v6.2.2 avec le mêmetexlive-full:2025.1. Contrairement aux invités QEMU, cette machine n’a pas de fréquence épinglée : c’est un serveur de classe production et nous la mesurons comme telle.
Deux précautions opérationnelles ont été nécessaires et méritent d’être mentionnées, car sans elles l’expérience mesure le harnais de test plutôt que le serveur. Premièrement, chaque conteneur a été confiné dans une slice systemd avec MemoryMax=940 GiB, afin qu’un balayage incontrôlé épuise un cgroup plutôt que l’hôte. Deuxièmement, les compilations en sandbox sont créées par le démon de l’hôte et chacune écrit dans sa propre couche copy-on-write — mesurée à 116 Mio par conteneur, même si l’image de base de 20,6 Gio est partagée — ; la racine de données Docker a donc été déplacée vers un périphérique NVMe dédié. Un balayage à écrit environ 119 Gio de couches temporaires, ce qui ne tient pas sur un système de fichiers racine standard.
3.3 Charge de travail
Le document est un véritable mémoire de master de 63 pages (modèle SJTU) compilé avec XeLaTeX vialatexmk, contenant des figures TikZ, un traitement bibliographique biblatex et des ressources PDF intégrées — autrement dit, une charge réaliste plutôt que synthétique. Une compilation unique sur un invité non chargé prend de 8,6 à 9,8 s selon les configurations, valeur que nous utilisons comme référence à vide .
3.4 Génération de charge
Nous créons 512 comptes utilisateurs réels et donnons à chacun sa propre copie du projet, afin que les compilations concurrentes entrent en concurrence exactement comme le feraient des utilisateurs indépendants, plutôt que de partager un verrou de projet. Les requêtes sont émises depuis l’hôte vers le port redirigé de l’invité, de sorte que la génération de charge ne consomme aucun CPU de l’invité. La concurrence est simultanée, et non échelonnée. Chaque session est d’abord établie — connexion, jeton CSRF, sélection du compilateur — et ce n’est qu’ensuite que chaque thread attend jusqu’à un instant d’horloge commun, calculé une fois et partagé, avant d’émettre sonPOST /project/:id/compile. La distinction n’a rien de pédant. Une rampe échelonnée mesure le débit sous une file stable ; une rafale simultanée mesure ce qui se passe lorsqu’un amphithéâtre d’étudiants appuie sur le même bouton après l’annonce de la même échéance, ce qui est le cas que les opérateurs redoutent réellement. Les deux diffèrent de plus qu’un facteur constant, car la seconde remplit la file de compilation plus vite que le démon ne peut la vider.
Quatre obstacles pratiques ont dû être levés avant que cette rafale puisse être délivrée fidèlement. Chacun mérite d’être consigné, car chacun dégrade silencieusement l’expérience en une mesure du harnais plutôt que du serveur.
3.4.1 Deux limiteurs de débit, et non un seul
Overleaf limite les connexions par adresse source — 20 tentatives par minute — et tout notre trafic provient d’un seul hôte. Attribuer à chaque utilisateur simulé une adresseX-Forwarded-For distincte lève cette limite, mais se heurte immédiatement à une seconde, plus grossière : un budget par sous-réseau d’environ 200 par minute. Répartir les utilisateurs sur un bloc contigu échoue donc au 201e compte. Nous dérivons plutôt l’adresse synthétique de l’indice de l’utilisateur, de sorte que des utilisateurs consécutifs tombent dans des /24 différents,
ce qui garde les deux limiteurs détendus pour toute la population de 1024.
3.4.2 L’en-tête injecté est ignoré par défaut
Définir l’en-tête ne suffit pas. Express ne prend en compteX-Forwarded-For que pour les pairs qu’on lui a demandé de considérer comme fiables, et le trustedProxyIps d’Overleaf vaut loopback par défaut. Comme le générateur de charge atteint l’application via le pont du conteneur plutôt que par l’interface loopback, l’en-tête est analysé puis jeté, et tous les utilisateurs simulés se retrouvent ramenés à une seule adresse. Le symptôme est une vague de HTTP 429 exactement à la vingtième connexion, facile à interpréter à tort comme une surcharge du serveur. Le réseau de la passerelle doit être ajouté explicitement à la chaîne de confiance ; dans le déploiement en cluster du §4.3, les CIDR des pods et des services doivent également être ajoutés.
3.4.3 Un répartiteur de charge écrasera l’en-tête qu’on lui demandait de préserver
Lorsque l’instance se trouve derrière un proxy, l’option classiqueoption forwardfor ajoute l’adresse réelle du client à la chaîne, ce qui est le comportement correct en production et précisément le mauvais ici : l’adresse synthétique est remplacée par celle du générateur de charge. La directive doit être précisée en option forwardfor if-none, afin que le proxy n’ajoute une valeur que lorsque le client n’en a fourni aucune.
3.4.4 Le client manque de descripteurs de fichiers avant que le serveur ne manque de capacité
À , le générateur maintient plus d’un millier de sockets simultanés, et la limite souple par défaut de 1024 descripteurs est atteinte pendant l’établissement des sessions plutôt que pendant la mesure. L’échec est discret : trois sessions ne parviennent pas à s’établir et l’exécution indique 1021 au lieu de 1024, tandis qu’un thread d’échantillonnage qui lance une commande externe pour compter les conteneurs meurt avecEMFILE et tronque silencieusement la télémétrie. La limite souple doit être relevée sur le générateur — la limite stricte de notre hôte était déjà de 1048576 — et l’exécution répétée. Nous rapportons les deux exécutions au §4.3 : celle corrigée complète 1024 sur 1024 avec une médiane à moins de 1,2 s de celle tronquée, c’est pourquoi nous considérons la première comme utilisable mais non faisant autorité.
3.5 Protocole de mesure
Plusieurs choix méthodologiques se sont révélés nécessaires pour la reproductibilité.3.5.1 Préchauffage
Sur un invité fraîchement démarré, le cache de pages est vide et les premières compilations mesurent les E/S de démarrage à froid plutôt que la capacité en régime établi : la même configuration 2 vCPU / 2 Gio donne 36,5 s à froid et 9,8 s à chaud, soit un facteur de 3,7. Chaque configuration effectue donc deux compilations uniques de préchauffage, ignorées, après le démarrage.3.5.2 Critère de réussite
Un niveau de concurrence n’est validé que si toutes les compilations réussissent et que le niveau survit à une répétition. C’est plus strict qu’un seuil de taux de réussite, et cela compte : à 4 vCPU / 16 Gio, un niveau de 32 a réussi une fois avec une médiane de 80,2 s, puis a dépassé le délai sur les 32 compilations lors de la répétition ; nous rapportons donc 31.3.5.3 Recherche
Les niveaux sont localisés par encadrement exponentiel à partir d’une valeur initiale prédite par le modèle, suivi d’une bissection entière exacte. Comme le critère est du tout ou rien, un niveau est tranché par son premier échec ; nous abandonnons donc les requêtes encore en cours dès qu’une échoue — sauf aux petits niveaux, où les compilations abandonnées bloquent tellement un petit invité qu’il ne s’en remet jamais.3.5.4 Isolation entre niveaux
Les conteneurs de compilation sont vidés, et l’application web est interrogée jusqu’à ce qu’elle réponde à nouveau, avant le début du niveau suivant. Sans cela, un niveau suivant un plantage enregistre un faux échec à zéro session.3.5.5 Hygiène de l’hôte
Les machines virtuelles sans rapport présentes sur l’hôte ont été arrêtées : avec 24 Gio de mémoire hôte engagés ailleurs, la même configuration d’invité affichait une charge moyenne de 11,7 au lieu de 3,2 à concurrence identique. La pression mémoire de l’hôte se propage dans l’invité et invalide la mesure.4. Résultats
4.1 La matrice de capacité
Le tableau 1 et la figure 4 donnent le plafond mesuré pour chaque configuration. La lecture d’une ligne constitue la première surprise. À 4 Gio, les invités à 2, 4 et 8 vCPU atteignent tous exactement 9 — quadrupler le nombre de cœurs ne change absolument rien. À 16 Gio, ils atteignent 54, 45 et 57 : passer de 4 à 16 cœurs rapporte 6 %, et l’invité à 8 cœurs est même moins bon que celui à 4 cœurs (§5.2). Ce n’est qu’à 48 Gio que le nombre de cœurs départage nettement les configurations : 143, 268 et 331.
Tableau 1. Nombre maximal de compilations simultanées réussies, mesuré avec un délai de compilation de 300 s et le plafond de concurrence CLSI levé. Le gras indique une configuration limitée par le CPU (les compilations dépassent le délai avec de la mémoire en réserve) ; les autres sont limitées par la mémoire (la pile meurt avec HTTP 502). La ligne 2 Gio intègre la correction discutée au §5.2.
4.2 La concurrence est du temps partagé
La figure 5 balaie chaque niveau de concurrence sur un invité fixe 8 vCPU / 16 Gio. Deux régimes sont séparés par un coude net à exactement une compilation par cœur. En dessous, le temps moyen de compilation est plat — il passe de 8,7 s à à 9,1 s à , soit une variation de 5 %. Au-dessus, le temps croît en stricte proportion de : à , nous mesurons 18,5 s et 27,1 s, soit un rapport de contre un idéal de .

4.3 Mise à l’échelle verticale jusqu’à 1024 compilations simultanées
Le tableau 2 et la figure 7 présentent le balayage sur le grand runner. Chaque niveau est une compilation à froid : avant chaque niveau, nous vidons le répertoire de compilation et le cache CLSI de chaque projet participant viaDELETE /project/:id/output, afin qu’aucun niveau ne profite du travail effectué par le niveau précédent. La référence en compilation unique sur cette machine est de 28,8 s ; il s’agit du chiffre à froid, qui ne doit pas être comparé à la référence en régime établi de 8,6–9,8 s utilisée précédemment ; la référence à froid sur les invités QEMU est de 28,3 s, de sorte que, par thread, les deux machines sont à deux pour cent l’une de l’autre pour cette charge de travail.
Tableau 2. Balayage de concurrence sur un EPYC 7773X (64 cœurs / 128 threads, 995 Gio). Tous les niveaux sont à froid ; référence 28,8 s. Le pic de conteneurs est le nombre maximal de sandboxes actives simultanément.

4.3.1 La machine n’échoue jamais
Chaque niveau se termine à 100 %, y compris — huit fois le nombre de threads. Nous n’avons pas trouvé le plafond de capacité de cette machine ; notre patience s’est épuisée avant sa marge. C’est la première configuration de l’étude où la contrainte limitante n’est pas la mémoire : à , le cgroup de compilation culmine à 184 Gio, un cinquième de sa limite de 940 Gio, tandis que le CPU est utilisé à 100 % avec une charge moyenne de 166.4.3.2 La dégradation est sous-linéaire car l’admission est limitée en débit
Un temps partagé naïf prédit que plus de threads coûte plus de latence. Le coût mesuré est de par rapport à une compilation unique, mais seulement de par rapport à — pour une charge proposée multipliée par huit. La raison est visible sur la figure 7(b) et dans la dernière colonne du tableau 2 : bien que 1024 requêtes soient émises simultanément, le nombre de sandboxes réellement actives ne dépasse jamais 205. Le démon ne peut pas créer des conteneurs aussi vite que les clients le demandent, si bien que les requêtes s’accumulent à l’admission au lieu de se disputer le CPU. C’est la mise en file d’attente qui sauve la queue de distribution ici, et ce de manière accidentelle.4.3.3 Le coude est à 512, pas au point d’échec
Entre et , la latence augmente de pour un doublement de la charge ; chaque doublement précédent coûtait entre et . Une capacité définie comme « le plus grand qui n’échoue pas » indiquerait 1024 et serait inutile pour un opérateur : à ce stade, l’attente en queue de distribution approche les huit minutes.4.4 Le temps de compilation est inversement proportionnel à la fréquence
Puisque la charge est limitée par le CPU, son coût devrait évoluer en . Nous le testons directement en balayant la fréquence de l’hôte sur toute la plage de la machine, de 1,0 à 5,5 GHz en dix paliers, sur un invité par ailleurs inchangé (figure 8). Le temps de compilation unique passe de 26,5 s à 4,8 s : une horloge 5,5× plus rapide apporte une accélération de 5,5× sans rendement décroissant nulle part sur la plage. Le produit est constant à 2 % près sur les dix fréquences. La normalisation par la part de cœur ramène les trente mesures — trois niveaux de concurrence à dix fréquences — sur une constante unique : avec une dispersion résiduelle de 5,1 % sur une plage où la fréquence elle-même varie d’un facteur 5,5. L’absence de toute courbure est en soi le résultat : si la charge avait été limitée par la bande passante mémoire ou par les E/S, se serait aplati à haute fréquence, le CPU dépassant l’autre ressource.
5. Analyse
5.1 Deux murs, ajustés séparément
Chaque configuration est classée selon sa signature d’échec (§2.3), puis le mur mémoire et le plafond CPU ne sont ajustés que sur les configurations qui les atteignent réellement : avec en gibioctets et en vCPU. L’exposant du mur mémoire est systématiquement super-linéaire, : le coût mémoire marginal d’une compilation concurrente supplémentaire diminue à mesure que la mémoire totale augmente, d’environ 312 Mio par compilation sur un invité de 3 Gio à environ 194 Mio sur un invité de 32 Gio. Le mécanisme est le cache de pages partagé sur l’arborescence TeX Live décrit au §2.1 : les compilations concurrentes lisent des fichiers de polices et de macros qui se recoupent, si bien qu’un cache plus grand est amorti sur un plus grand nombre d’entre elles. C’est pourquoi la règle naïve « un gigaoctet pour cinq utilisateurs » sous-estime les grandes machines et surestime les petites.
5.2 Quand davantage de cœurs aggravent la situation
L’équation (2) est un minimum de deux termes et donc monotone en , mais les mesures ne le sont pas. Nous observons deux inversions où l’ajout de cœurs a réduit la capacité : à 16 Gio (54 contre 45) et à 32 Gio (145 contre 135). Toutes deux se produisent dans le régime limité par la mémoire, et le mécanisme est le même : avec plus de cœurs, les compilations concurrentes progressent en parfaite synchronie et atteignent leur taille résidente maximale au même moment, alors qu’avec moins de cœurs l’ordonnanceur les entrelace et les pics sont décalés. Sur un invité dont la marge mémoire est déjà faible, ce décalage est ce qui le maintient en vie. Un modèle de capacité fondé sur l’utilisation moyenne des ressources ne peut pas l’exprimer ; c’est une propriété de la coïncidence des pics. Une troisième inversion apparente, à 2 Gio, est désormais écartée. La recherche enregistre une capacité de 2 à 2 vCPU mais de 1 à 4 et 8 vCPU, ce qui ressemble au même effet. Un réexamen des balayages bruts montre quelque chose de plus simple : à 2 Gio, le niveau a réussi à la première tentative pour les trois nombres de cœurs, puis a échoué à sa confirmation pour deux des trois. Ce niveau n’est pas une capacité mais un pile ou face, et l’entrée à 2 vCPU est le lancer qui est tombé du bon côté. Nous rapportons donc la valeur reproductible, 1, pour les trois nombres de cœurs, et ne tirons aucune conclusion de la différence. Nous consignons la correction ici plutôt que de modifier silencieusement le tableau, car la lecture écartée est du genre qui aurait étayé une affirmation intéressante.6. Constats d’implémentation
6.1 Un plafond de concurrence codé en dur
Sur des invités suffisamment grands, la capacité s’arrêtait à exactement 65 compilations simultanées, quelle que soit la concurrence demandée : à , nous avons mesuré réussites et réponsesunavailable immédiates, avec un nombre de conteneurs bloqué à 65, plusieurs gigaoctets de mémoire inutilisés, et un temps de compilation médian stable à 77 s — bien en dessous de tout délai.
La cause est une constante dans CLSI :
success=65, unavailable=15 à a indiqué success=80.
6.2 Une limite de mémoire de conteneur inopérante
L’inspection d’un conteneur de compilation actif ne montre aucune isolation des ressources :HostConfig, où l’API Docker l’attend, si bien qu’il est ignoré — ce que confirme le Memory=0 observé. Les deux erreurs sont présentes dans le commit qui a introduit le fichier (9a519f0d3d, mars 2018) et ont survécu à la conversion depuis CoffeeScript, à un reformatage de tout le dépôt et à une migration de CJS vers ESM, aucune de ces opérations ne revisitant la sémantique. Notons que MAX_OUTPUT = 1024 * 1024 // 1MB dans le même commit est correct, ce qui indique une étourderie plutôt qu’une incompréhension.
La conséquence est visible dans nos mesures à faible mémoire. Comme les compilations ne sont pas bornées, l’épuisement de la mémoire ne se manifeste pas par l’arrêt par Docker d’un conteneur fautif ; il fait tomber l’invité entier. Sur la configuration 2 vCPU / 2 Gio, nous avons observé la session SSH de supervision bloquée pendant 300 s, une charge moyenne de 68 sur deux cœurs, et l’invité finissant par redémarrer de lui-même. Une limite par conteneur fonctionnelle dégraderait le service de manière bien plus progressive : la compilation trop volumineuse échouerait et le service survivrait.
La seule limite qui prend effet est RLIMIT_CPU, fixée à secondes. Elle borne le temps CPU, pas le temps réel, et une compilation unique ne consomme qu’environ 9 s de CPU ; elle n’est donc jamais limitante, quelle que soit la concurrence ; elle protège contre des entrées pathologiques comme une macro qui s’emballe. C’est toutefois un oracle utile : observer Soft:305 confirme qu’un réglage de délai à 300 s s’est effectivement propagé jusqu’au conteneur.
6.3 Le délai de compilation est le paramètre dominant
Le champ par utilisateurfeatures.compileTimeout vaut 180 s par défaut. Pour toute configuration limitée par le CPU, ce n’est pas une marge de sécurité mais un réglage de capacité, car une machine qui calcule encore correctement est déclarée en échec. Le porter à 300 s — une simple mise à jour MongoDB — modifie la capacité mesurée d’un facteur pouvant atteindre 4,2 (tableau 3). Le plafond est de 600 s, imposé par RequestParser.MAX_TIMEOUT, au-delà duquel la valeur est silencieusement tronquée.
Tableau 3. Effet du délai de compilation sur la capacité mesurée.
Les deux dernières lignes constituent la moitié contre-intuitive du résultat et la raison pour laquelle nous avons remesuré chaque configuration sous un même délai. Pour les configurations limitées par la mémoire, un délai plus long réduit la capacité, car chaque compilation conserve son ensemble résident plus longtemps et davantage d’entre elles se chevauchent. Un chiffre de capacité n’a donc aucun sens sans indiquer le délai sous lequel il a été mesuré, et les deux ne peuvent pas être mélangés dans un même tableau.
7. Travaux connexes
7.1 Recommandations de l’éditeur
La documentation matérielle d’Overleaf énonce elle-même les faits qualitatifs que nous quantifions ici : que LaTeX est monothread, que les performances monocœur gouvernent donc le temps de compilation, et que « more cores will only help if you are trying to compile more documents than you have free CPU cores » [1]. Elle donne ensuite la règle de dimensionnement linéaire — une base de 2 cœurs/3 Gio plus un cœur et un gigaoctet pour cinq à dix utilisateurs simultanés — qui a motivé cette étude. Notre contribution consiste à transformer ces énoncés en lois mesurées (équations (1) et (2)), et à montrer où la règle linéaire échoue : elle ne comporte aucun terme pour le cache de pages partagé qui rend le mur mémoire super-linéaire, ni pour les deux paramètres logiciels qui dominent le résultat.7.2 Études de capacité de build et de CI
La mesure des systèmes de build sous concurrence est bien établie en dehors du contexte LaTeX. LightSys rapporte que les systèmes de CI conventionnels compilant dans des conteneurs Docker voient leurs E/S se dégrader à mesure que le rythme d’arrivée des pull requests augmente, avec un goulot d’étranglement apparaissant vers onze requêtes simultanées [17] ; TAOS-CI observe que la compilation domine le temps réel de la CI, représentant 60 à 67 % de la durée totale du pipeline sur les grands projets [18]. Notre système diffère sur un point qui s’avère décisif : une compilation LaTeX est interactive. Un job de CI deux fois plus long est un désagrément ; une compilation deux fois plus longue est observée directement par un utilisateur qui attend devant un volet d’aperçu, c’est pourquoi nous traitons le délai non pas comme un seuil d’échec mais comme un paramètre de capacité.7.3 Surcoûts des conteneurs
Des travaux récents décomposent la latence de démarrage des conteneurs Docker selon les niveaux de stockage [19] et caractérisent les performances des conteneurs en périphérie [20]. Dans notre contexte, le démarrage du conteneur par compilation est amorti : c’est une petite constante par rapport à une compilation de 9 s, et le temps à vide que nous ajustons l’absorbe. La propriété du conteneur qui compte réellement est l’absence de limites de ressources (§6.2), qui transforme un dépassement de mémoire d’une compilation en une défaillance de tout l’hôte.7.4 LaTeX comme entrée non fiable
La compilation en sandbox existe parce que TeX est un langage de programmation et que les documents sont des entrées non fiables [21, 22]. Ce choix de conception est ce qui rend cette étude possible — chaque compilation est un conteneur isolé au comportement de ressources observable — et aussi ce qui rend lourde de conséquences l’absence de limite mémoire, puisque l’isolation est supposée par les opérateurs qui le déploient.7.5 Le compilateur comme objet d’étude
TeX lui-même est bien documenté en tant que langage [16], mais son comportement en tant que cible de build n’a attiré l’attention que récemment. Tan et Rigger [8] compilent un vaste corpus de sources arXiv avec différents moteurs et versions de distribution, et constatent que le choix du moteur n’est pas interchangeable : seule une fraction de pour cent des documents produisent une sortie identique à l’octet près sous XeTeX et pdfTeX. Ce résultat concerne directement notre méthodologie. La capacité est une propriété d’un document et d’un moteur, donc un benchmark qui ne fixe pas les deux n’est pas reproductible ; nous fixons par conséquent un document, un moteur et une distribution (texlive-full:2025.1) tout au long de l’étude, et nous indiquons le moteur dans chaque légende de figure. Cela borne aussi la généralité de nos chiffres d’une manière qu’il convient d’énoncer clairement : ils caractérisent XeLaTeX sur ce document, et non TeX dans l’abstrait.
Les travaux sur les systèmes de build LaTeX sont largement portés par les praticiens. Le l3build du projet LaTeX3 [13] standardise les tests de régression et l’empaquetage, et des benchmarks indépendants comparent les outils d’enrobage — une étude de 26 systèmes de build constate qu’un préambule précompilé apporte environ 20 % par rapport à une exécution simple et 40 % par rapport à latexmk [14]. Ces outils optimisent la compilation unique. Ils sont orthogonaux à ce que nous mesurons et s’y combinent : un cache de préambule raccourcit , et chaque chiffre de capacité de cet article évolue avec .
7.6 Le contrôle de concurrence dans l’éditeur, pas dans le compilateur
La moitié collaborative d’Overleaf repose sur une lignée de travaux bien établie. La transformation opérationnelle trouve son origine chez Ellis et Gibbs [9] et a été rendue pratique pour des clients à forte latence par le système Jupiter [10], dont la conception est reconnaissable dansdocument-updater : un serveur qui ordonne les opérations et un tampon par document avec lequel les clients se synchronisent. Les types de données répliqués sans conflit [11] résolvent le même problème sans séquenceur central. Cette distinction est ce qui permet à la topologie du §4.3 de fonctionner : comme le tampon des mises à jour en attente réside dans un Redis partagé plutôt que dans la mémoire d’une instance, une compilation acheminée vers n’importe quel réplica observe les dernières frappes, et l’affinité de compilation peut être choisie pour la localité du cache plutôt que pour la cohérence.
7.7 Modèles de capacité
La loi d’Amdahl [24] borne l’accélération obtenue par parallélisme et la loi de Little [23] relie l’occupation au rythme d’arrivée et au temps de service ; toutes deux sont utilisées ci-dessus. La loi d’évolutivité universelle de Gunther [12] étend la première avec un terme rétrograde pour le délai de cohérence, prédisant que le débit atteint un pic puis décline. Nous notons que notre système ne présente pas ce régime rétrograde jusqu’à : le débit sature et la latence augmente, mais rien ne s’effondre. La raison est structurelle plutôt que fortuite — les compilations ne partagent aucun état à maintenir cohérent, de sorte que le terme ajouté par la loi est proche de zéro, et le plateau d’admission du §4.3 plafonne la contention avant qu’elle ne puisse compter.8. Recommandations pour les opérateurs
1
Corrigez les deux paramètres logiciels avant d'acheter du matériel
Les deux sont gratuits et chacun vaut plus que n’importe quelle mise à niveau matérielle que nous avons mesurée. Portez
features.compileTimeout à une valeur que vos utilisateurs toléreront réellement — le maximum accepté par CLSI est de 600 s — et, si vous prévoyez de dépasser 65 compilations simultanées, levez compileConcurrencyLimit dans une image dérivée ou passez à l’échelle horizontalement. Ne faire ni l’un ni l’autre revient à payer pour des cœurs que le logiciel refuse d’utiliser.2
Dimensionnez une machine selon son coude, pas selon son plafond
Le balayage sur le grand runner (§4.3) sépare deux chiffres couramment confondus. Le plafond — la plus grande concurrence qui renvoie encore chaque PDF — est d’au moins 1024 sur un serveur de 64 cœurs, et nous ne l’avons jamais atteint. Le coude — le point au-delà duquel la latence de queue cesse de croître doucement et commence à doubler — se situe à 512, et le dernier point de fonctionnement confortable en dessous est 256. Entre et , l’attente passe de deux minutes à près de six ; entre 512 et 1024, elle atteint huit. Un opérateur qui dimensionne selon le plafond livre un système qui fonctionne techniquement et que personne ne veut utiliser.Pour cette machine et ce document, le point de fonctionnement recommandé est donc de 256 compilations simultanées, soit le nombre de cœurs physiques et le nombre de threads, ce qui maintient autour de 120 s. Nous suggérons de fixer
compileConcurrencyLimit à cette valeur plutôt que de la laisser élevée : admettre 1024 compilations d’un coup fait attendre tout le monde huit minutes, alors qu’en admettre 256 et mettre le reste en file d’attente sert la plupart des utilisateurs en deux. La file d’attente pénalise les retardataires ; la contention pénalise tout le monde.3
Considérez ces chiffres comme le pire cas
Chaque niveau du tableau 2 est une compilation à froid lancée simultanément. Aucune de ces conditions n’est réunie en production : une compilation à chaud du même document prend 8,6 s contre 28,3 s à froid, soit un facteur de , et les vrais utilisateurs n’appuient pas sur le bouton à la même seconde. Une population en régime établi qui recompile toutes les deux minutes avec un taux de succès de cache typique soutiendra donc bien plus de rédacteurs que le seul chiffre de concurrence ne le suggère — de l’ordre d’un millier d’auteurs actifs ou plus au point de fonctionnement de 256. Le chiffre de concurrence est une borne sur la rafale instantanée, pas un nombre de places.
4
Décidez d'abord du budget de latence, puis déduisez-en la taille
L’équation (1) s’inverse directement. Pour une attente cible à la fréquence sur cœurs, la concurrence admissible est avec pour ce document. Un budget de 60 s sur 8 cœurs à 3 GHz donne ; un budget de 120 s le double. Publier le budget avec la capacité est la seule manière honnête d’énoncer l’un ou l’autre.
5
Achetez d'abord de la mémoire, puis des cœurs, et vérifiez quel mur vous atteignez
En dessous de 32 Gio, nous n’avons mesuré presque aucun bénéfice à ajouter des cœurs. Le diagnostic est peu coûteux : si les échecs apparaissent sous forme de HTTP 502 avec un invité à court de mémoire, ajoutez de la mémoire ; s’ils apparaissent sous forme de
timedout avec de la mémoire en réserve, ajoutez des cœurs ou augmentez le délai. Les opérateurs peuvent le lire dans la même signature d’échec que celle que nous avons utilisée pour classer les configurations.6
Privilégiez la fréquence pour l'expérience, les cœurs pour le nombre d'utilisateurs
Comme se vérifie sans courbure (2 % sur 1,0–5,5 GHz), une fréquence plus élevée accélère chaque compilation pour chaque utilisateur. Davantage de cœurs n’accélèrent aucune compilation individuelle ; ils ne font qu’en admettre davantage simultanément. Les déploiements dont la plainte est « les compilations sont lentes » devraient acheter de la fréquence ; ceux dont la plainte est « les compilations échouent à l’approche des échéances » devraient acheter de la mémoire et des cœurs.
7
Passez à l'échelle horizontalement plutôt que verticalement au-delà du plafond
Au-delà de 65 compilations simultanées, la voie prise en charge est la mise à l’échelle horizontale (figures 2c et 10, détaillées au §9) : plusieurs instances applicatives derrière un répartiteur de charge avec affinité de session par cookie, partageant un MongoDB, un Redis et un stockage compatible S3 centraux, avec
git-bridge laissé en instance unique. Cela multiplie le plafond par instance par le nombre d’instances, ce qui est exactement la manière dont le déploiement SaaS atteint sa propre capacité.8
Ne comptez pas sur l'isolation par compilation
Tant que la limite mémoire du runner Docker n’est pas corrigée (§6.2), un seul document pathologique peut épuiser l’hôte au lieu d’être tué isolément. Les opérateurs qui ont besoin de cette garantie devraient l’imposer eux-mêmes plutôt que de l’attendre. Le mécanisme que nous avons utilisé sur le grand hôte est une slice systemd portant un plafond strict, vers laquelle le démon Docker est ensuite dirigé afin que chaque conteneur qu’il crée y soit comptabilisé :Un détail de ce dispositif coûte un après-midi s’il est manqué. Une slice nommée
docker-capped.slice ne se trouve pas à côté de docker.slice ; elle se trouve à l’intérieur, car le tiret est le séparateur de hiérarchie et non une partie du nom. Un plafond qui semble sans effet a généralement été appliqué à un niveau de distance de l’endroit où vivent réellement les conteneurs. Vérifiez en relisant le pic dans memory.max_usage_in_bytes après une exécution plutôt qu’en vous fiant au fichier de configuration — sur notre hôte, le cgroup de compilation n’a jamais dépassé un cinquième de son plafond, même à 1024 compilations simultanées, ce qui prouve en soi que c’est le démon, et non la mémoire, qui était la contrainte limitante.
git-bridge, qui conserve les dépôts sur disque local sans possibilité de réplication et doit fonctionner en instance unique à côté d’un réplica désigné.
9. Un déploiement multi-machines de référence
Tout ce qui précède mesure une seule machine. Cette section décrit la forme distribuée avec suffisamment de détails pour la construire et — parce que la question à laquelle un opérateur est réellement confronté n’est pas comment mais s’il faut le faire — indique d’abord le point à partir duquel cela en vaut la peine.9.1 Quand la forme distribuée se justifie
Une machine unique est moins coûteuse à exploiter à tous les égards qui comptent : un seul domaine de défaillance, aucun état partagé à maintenir cohérent, aucun routage à mal configurer. Nos données fixent trois seuils pour décider de la quitter.9.1.1 En dessous de 65 compilations simultanées, ne le faites pas
Le plafond par instance est une constante logicielle, pas matérielle (§6.1). Tant que la charge proposée n’en approche pas, une seconde machine ajoute des modes de défaillance et n’apporte rien. L’hôte de 64 cœurs n’a servi 256 compilations simultanées avec un succès complet qu’après la levée decompileConcurrencyLimit ; un opérateur qui n’a pas encore modifié cette unique valeur n’est pas limité par le matériel et ne devrait pas en acheter.
9.1.2 Entre 65 et environ 500, passez d’abord à l’échelle verticalement
La mise à l’échelle verticale est restée linéaire sur toute notre plage et n’est jamais entrée dans un régime rétrograde. Un seul grand hôte a atteint 1024 compilations à froid simultanées avec 100 % de réussite (§4.3) ; le coude de latence est apparu à 512, pas avant. Dans cette fourchette, une machine plus grande est strictement plus simple que plusieurs plus petites et, d’après le §4.4, une machine plus rapide améliore l’expérience de chaque utilisateur au lieu de simplement en admettre davantage.9.1.3 Passez au distribué pour la disponibilité, pas pour le débit
La raison honnête de faire tourner plus d’un réplica applicatif en dessous du plafond est qu’une machine, c’est une alimentation, un noyau, une fenêtre de mise à niveau. C’est une raison légitime et c’est celle que nous donnerions ; ce n’est simplement pas un argument de capacité, et confondre les deux conduit les opérateurs à acheter des réplicas alors qu’ils avaient besoin de mémoire.9.2 Les niveaux et leur dimensionnement
La figure 10 montre la topologie. Elle comporte quatre niveaux, qui évoluent selon des grandeurs différentes — ce qui est tout l’intérêt de les séparer.9.2.1 Périphérie
Un répartiteur de charge, ou deux pour la disponibilité. Il termine TLS et ne fait rien de coûteux ; il évolue avec le nombre de connexions, pas avec le nombre de compilations, et une petite instance suffit pour les charges étudiées ici. C’est sa configuration, et non sa taille, qui compte (§9.3).9.2.2 Réplicas applicatifs
Ils portent la charge de compilation et constituent le seul niveau qui évolue avec la concurrence. Dimensionnez chacun selon les règles du §8 — la mémoire avant les cœurs, puis la fréquence — puis fixez le nombre de réplicas pour couvrir la concurrence de pointe divisée par le plafond par réplica. Les réplicas ne conservent rien de durable : leur disque local contient les fichiers temporaires de compilation et un cache de sortie, tous deux reconstructibles. C’est ce qui permet de les ajouter et de les retirer librement en toute sécurité, et cela mérite d’être vérifié plutôt que supposé, car un seul cheminfilestore mal configuré transforme silencieusement ce niveau en niveau avec état.
9.2.3 État
Redis, MongoDB et un stockage objet compatible S3, sur des hôtes séparés. Redis est le pilier, et le moins évident : il contient le magasin de sessions et le tampon des documents en direct, ce qui permet à une compilation acheminée vers n’importe quel réplica d’observer les frappes saisies sur un autre réplica. Un opérateur qui traite Redis comme un cache et le dimensionne pour l’éviction produira des compilations de documents périmés extrêmement difficiles à diagnostiquer, car rien n’échoue — la sortie est simplement fausse. MongoDB évolue avec le nombre de projets plutôt qu’avec le rythme de compilation. Le stockage objet est optionnel avec un réplica et obligatoire au-delà.9.2.4 L’instance unique
git-bridge conserve les dépôts sur disque local, maintient un index local et n’a aucune possibilité de réplication. Il doit fonctionner en exactement une instance, épinglée à côté d’un réplica désigné, et c’est le composant qui rend le déploiement pas tout à fait sans état. Planifiez son hôte en conséquence : c’est son disque qu’il faut sauvegarder.
Tableau 4. Niveaux de référence. Seul le niveau applicatif évolue avec la concurrence ; son dimensionnement fait l’objet du §8.
9.3 Le routage est la partie qu’il est facile de mal faire
Trois catégories de requêtes doivent atteindre trois endroits différents, et la configuration par défaut à règle unique en satisfait au mieux deux. Le trafic de compilation sous/project/ doit être distribué par hachage cohérent sur l’identifiant du projet, afin que le cache de compilation d’un projet reste sur un même réplica. Nous utilisons balance hash path,field(3,/) de HAProxy avec hash-type consistent et hash-balance-factor 150. Ce choix compte lors de l’ajout de réplicas : avec l’affinité par cookie, les sessions existantes restent indéfiniment épinglées à leur réplica d’origine et un réplica nouvellement ajouté ne reçoit que les nouveaux utilisateurs, si bien que la machine que l’opérateur vient de payer n’absorbe rien de la charge qui a motivé son achat. Le hachage cohérent a redistribué 35 % des projets lors de l’ajout dans notre configuration, contre 0 % pour les cookies.
Le trafic de session est différent. Lorsque la mise à niveau WebSocket échoue et que socket.io se rabat sur le polling XHR, les requêtes successives d’une même session doivent atteindre un même réplica, et il n’y a aucun identifiant de projet dans le chemin à hacher. Ce trafic nécessite un backend séparé avec affinité par cookie. Nous avons conçu cette séparation mais ne l’avons pas déployée ; nous la signalons comme une lacune plutôt que de la revendiquer.
Enfin, /git/ doit atteindre le réplica à côté duquel tourne git-bridge. Il est routé vers ce réplica plutôt que directement vers git-bridge, car le bridge authentifie ses rappels auprès des points de terminaison OAuth de l’application et résout les URL de blobs par son intermédiaire ; contourner le réplica casse l’authentification au lieu d’améliorer quoi que ce soit.
9.4 La réduction d’échelle nécessite un délai de drainage
Retirer un réplica n’est pas symétrique à en ajouter un : une compilation en cours est perdue, et l’utilisateur voit un échec dont il n’est pas la cause. La séquence viable consiste à arrêter d’abord le nouveau trafic, attendre, et seulement ensuite terminer. Nous l’avons implémentée sous forme d’un hook pre-stop qui retient le pod pendant un intervalle configurable tandis que le répartiteur marque le backend comme en cours de drainage — assez court pour être testé en quelques minutes et, en production, assez long pour la fin naturelle d’une session, des heures plutôt que des secondes. Cet intervalle est le réglage qui décide si l’élasticité est invisible ou exaspérante. Nous avons découvert une contrainte supplémentaire par la mesure plutôt que par la conception : l’autoscaling sur le CPU ne fonctionne pas pour cette charge. L’utilisation propre du pod applicatif était de 22 m cœur contre un total de nœud de 3997 m cœur, car le travail de compilation se déroule dans des conteneurs frères que le pod ne comptabilise pas. Tout signal utilisé pour faire évoluer ce niveau doit compter les conteneurs de compilation en cours d’exécution, et non le CPU du pod.10. Implications au-delà d’Overleaf
Rien dans les §4.2 et §4.3 n’est spécifique au code d’Overleaf. Les lois mesurées découlent de trois propriétés que partage tout service LaTeX hébergé : l’unité de travail est un processus monothread, elle est isolée dans un conteneur, et son ensemble de travail est une grande arborescence en lecture seule que le cache de pages doit contenir. Trois conséquences s’appliquent directement à quiconque construit un tel service.10.1 Provisionnez la mémoire, puis les cœurs
Le résultat le plus fort de la matrice est négatif : en dessous de 16 Gio, le nombre de cœurs est presque sans importance, et ce n’est qu’à 48 Gio que les configurations à 4, 8 et 16 vCPU se distinguent (143, 268, 331). Un opérateur qui lit la règle conventionnelle comme « ajouter un cœur pour cinq utilisateurs » achète la mauvaise ressource. Le mécanisme est le cache de pages partagé sur l’arborescence de la distribution, et c’est une propriété de la taille de TeX Live plutôt que d’une interface particulière.10.2 Le rythme d’admission est une ressource, et on l’oublie généralement
À , notre serveur n’a jamais maintenu plus de 205 sandboxes actives (figure 7b), alors que toutes les requêtes sont arrivées en même temps. C’est la création de conteneurs, et non la compilation, qui était le facteur limitant — ce qui concorde avec les études de mesure qui attribuent le coût de démarrage des conteneurs au surcoût du runtime plutôt qu’à la taille de l’image [19, 20]. Un service qui ne dimensionne que le CPU et la mémoire verra son comportement en rafale gouverné par une grandeur qu’il n’a jamais mesurée. La forme pratique en est la recommandation du §8 : plafonnez délibérément l’admission, car une file d’attente que vous choisissez vaut mieux qu’une file d’attente que vous découvrez.10.3 Une sandbox qui survit à sa compilation invalide le modèle
Chaque chiffre de capacité présenté ici suppose que le conteneur est créé, effectue une compilation et se termine — une durée de vie de quelques dizaines de secondes et un taux d’occupation proche de un uniquement pendant son exécution. Deux modèles de conception récents brisent cette hypothèse, et ils la brisent de la même manière. Le premier est la sandbox persistante par utilisateur. Attribuer à chaque utilisateur un environnement privé fixe transforme un pool multiplexé statistiquement en un ensemble de réservations : un service capable de servir 256 compilations simultanées sur 64 cœurs par temps partagé ne peut servir que 16 utilisateurs si chacun reçoit quatre cœurs dédiés, soit un ordre de grandeur de moins pour le même matériel. Nos données quantifient le coût de ce choix plutôt que de s’y opposer — les réservations achètent de la prévisibilité, et le taux de change est d’environ au point de fonctionnement que nous recommandons. Le second, plus récent, est l’agent d’IA qui partage la sandbox avec le compilateur. Dans les plateformes de rédaction assistées par agent, le même conteneur qui exécute XeLaTeX peut aussi héberger un agent de codage de longue durée, de sorte qu’il est occupé en continu plutôt que par rafales. Les praticiens rapportent exactement le symptôme que le modèle prédit pour de tels déploiements — une lenteur persistante avec un nombre modeste d’utilisateurs [15]. L’interaction mérite d’être énoncée précisément, car il ne s’agit pas simplement de « plus de charge ». Trois de nos constats se cumulent. L’occupation cesse d’être en rafales, si bien que la loi de temps partagé du §4.2 s’applique à toute la population à la fois plutôt qu’à la fraction en train de compiler. Le cache de pages, qui procure le rendement mémoire super-linéaire du §4.1, est désormais partagé avec l’ensemble de travail propre de l’agent et cesse d’être chaud pour TeX. Et l’absence de limite mémoire de conteneur du §6.2 devient bien plus dangereuse, car un conteneur qui ne se termine jamais ne rend jamais sa mémoire. Nous n’avons pas mesuré une telle plateforme et ne formulons aucune affirmation sur un produit particulier. Ce que nous pouvons dire, c’est ce que nos chiffres impliquent pour la conception : une architecture qui donne à chaque utilisateur une sandbox multicœur de longue durée doit être dimensionnée comme un système de réservation, et non selon les chiffres de concurrence rapportés ici, et la capacité qu’elle peut espérer est plus proche de son nombre de cœurs divisé par le nombre de cœurs par utilisateur que de quoi que ce soit dans le tableau 1.11. Menaces à la validité
11.1 Document unique
Toutes les mesures utilisent un seul document XeLaTeX de 63 pages. Les capacités absolues différeront pour d’autres documents ; les lois d’évolution, qui sont des rapports, ne devraient pas différer. Un document avec un ensemble résident nettement plus grand déplacerait le mur mémoire sans changer son caractère super-linéaire.11.2 Hôte virtualisé
Les invités tournent sous KVM sur une seule machine physique ; les chiffres absolus incluent donc le surcoût de la virtualisation, et les invités partagent le cache de pages et le périphérique NVMe de l’hôte. Nous avons atténué le principal facteur de confusion en arrêtant les invités sans rapport, après avoir constaté que la pression mémoire de l’hôte gonfle les charges moyennes dans l’invité de plus de à concurrence identique.11.3 Arrivée simultanée
Chaque compilation est émise au même instant, ce qui constitue le pire cas. Les vrais utilisateurs arrivent selon un processus stochastique ; un déploiement dimensionné selon nos chiffres dispose donc d’une marge plutôt que d’un déficit — mais le pic à l’approche d’une date limite de soumission est plus proche de notre modèle que d’un modèle de Poisson.11.4 Configurations limites
À 2 Gio, le système est si proche de l’effondrement que des exécutions répétées de la même configuration peuvent différer d’une compilation. Nous rapportons la valeur prudente et ne tirons aucune conclusion des différences de dans ce régime.12. Disponibilité
Le système testé, l’outillage de déploiement et le projet amont dont il dérive sont tous publics :- Ayakaleaf Pro — https://github.com/ayaka-notes/ayakaleaf-pro
- Toolkit de déploiement — https://github.com/ayaka-notes/toolkit
- Documentation — https://ayakaleaf-pro.ayaka.space
- Overleaf amont — https://github.com/overleaf/overleaf
- Images de compilation TeX Live —
ghcr.io/ayaka-notes/texlive-full:2025.1
9a519f0d3d, 5d472e9b38) sont accessibles dans l’historique d’Overleaf.

