> For the complete documentation index, see [llms.txt](https://ayakaleaf-pro.ayaka.space/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ayakaleaf-pro.ayaka.space/blog/fr/2026/overleaf-benchmark.md).

# Benchmark Overleaf : une étude approfondie de la compilation concurrente de LaTeX dans Overleaf (SaaS et auto-hébergé)

{% file src="/files/7d94823f24ee10759ba02a8e4b2c0abf5e77a0e7" %}

## Résumé

Les déploiements Overleaf auto-hébergés sont généralement dimensionnés à l’aide d’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, parce qu’elle suppose qu’une seule dimension de ressource gouverne la capacité alors que, en réalité, deux murs indépendants le font, et parce que deux paramètres logiciels — dont aucun n’est matériel — dominent le résultat d’un facteur allant jusqu’à quatre.

Nous mesurons un déploiement standard d’Ayakaleaf Pro v6.2.2 avec compilation en bac à sable (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 vraie 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é, 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 plutôt gouvernée par un mur mémoire superlinéaire provenant du cache de pages partagé sur l’arborescence TeX Live.

Pour vérifier si ces lois survivent à un changement d’échelle d’un ordre de grandeur, nous répétons le balayage sur un seul 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 sa limite. Le nombre utile n’est pas cette limite, mais le coude en dessous : la latence de queue augmente de 20 à 40 % par doublement jusqu’à $$N=256$$puis de 190 % à $$N=512$$. La capacité indiquée comme « la plus grande concurrence qui ne tombe pas en échec » surestimerait donc le point d’exploitation utilisable d’un facteur quatre. Sur cette machine, la mémoire n’est jamais la ressource limitante ; la limite est le CPU, ainsi que la vitesse à laquelle le démon de conteneurs peut admettre de nouveaux bacs à sable, ce qui se sature près de 200 quel que soit le nombre de compilations demandées.

Nous identifions en outre deux effets au niveau de l’implémentation, invisibles pour le dimensionnement 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 un HTTP 503 au lieu d’être mis en file d’attente. Deuxièmement, la limite mémoire par conteneur dans le runner Docker est inefficace depuis son introduction en 2018, à la fois par son ampleur et par son emplacement, si bien qu’un événement de manque de mémoire met à terre l’hôte entier plutôt qu’une seule compilation. Lever le plafond de concurrence et augmenter le délai d’expiration de compilation par défaut de 180 s à 300 s fait passer la capacité mesurée d’un invité 8 vCPU / 48 Gio de 64 à 268 compilations simultanées — un facteur 4,2 à coût matériel nul.

Enfin, nous montrons que la concurrence dans ce système n’apporte rien d’autre que le partage du temps, et que la charge est limitée par l’horloge seule. Une loi de dégradation ajustée $$T(N)=T\_1\max(1,N/C)^{b}$$ donne $$b=0.914$$une quasi-certaine proportionnalité du ralentissement, et un balayage de l’horloge sur toute la plage de la machine, 1,0–5,5 GHz, fait se superposer trente mesures sur $$T=(k/f)\max(1,N/C)$$ avec $$k=27.9 GHz·s$$ et une dispersion résiduelle de 5,1 %. Une horloge 5,5× plus rapide procure une accélération 5,5× plus grande sans rendement décroissant, ce qui est le sens dans lequel l’horloge et les cœurs achètent des choses différentes : l’horloge accélère la compilation de chaque utilisateur, les cœurs n’en admettent que davantage.

## 1. Introduction

Overleaf est l’éditeur LaTeX collaboratif dominant, et sa distribution sur site est largement déployée par des universités et des groupes de recherche qui ne peuvent pas envoyer des manuscrits inédits à un cloud tiers. Le dimensionnement d’un tel déploiement est une question pratique récurrente : avec un budget matériel fixe, combien de personnes peuvent réellement appuyer sur « Recompiler » 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 suppose que la capacité croît de manière fluide et conjointe selon ces deux ressources. Nos mesures contredisent cela de trois façons.

### 1.1 La capacité est gouvernée par deux murs indépendants, pas 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 d’expiration côté serveur, auquel cas CLSI signale `délai expiré` alors que des gigaoctets de mémoire restent inutilisés. Ces deux régimes ont un comportement de mise à l’échelle totalement différent et des remèdes différents. Ajouter des cœurs à une configuration contrainte par la mémoire n’est pas seulement inefficace, c’est parfois contre-productif : nous mesurons des configurations où augmenter le nombre de cœurs *réduit* la capacité, parce que davantage de cœurs font progresser les compilations simultanées de manière synchrone, si bien que leurs besoins mémoire de pointe coïncident au lieu de s’entrelacer.

### 1.2 Les paramètres logiciels dominent le matériel

Le délai d’expiration 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 à cause d’une constante codée en dur. Toute étude de capacité — et tout déploiement — qui ne tient pas compte des deux mesure le logiciel, pas la machine.

### 1.3 La concurrence est un partage du temps, pas du parallélisme

Parce qu’une compilation LaTeX est monothread, servir $$N$$ des utilisateurs simultanés sur $$C$$ des cœurs ne fait pas terminer le système plus vite ; cela fait simplement attendre chaque utilisateur proportionnellement plus longtemps. La question « combien d’utilisateurs simultanés sont pris en charge » n’a donc pas de sens tant qu’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ée dans des conditions verrouillées sur l’horloge 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 superlinéaire d’un plafond CPU, et un modèle de latence établissant un comportement de pur partage du temps.
* L’identification et la confirmation expérimentale de deux problèmes d’implémentation dans le système déployé, dont une limite mémoire de conteneur inopérante depuis 2018.
* Une quantification du compromis entre délai de compilation et capacité, que nous estimons devoir être exprimée en même temps que toute valeur de concurrence.

## 2. Contexte

### 2.1 Chaîne de compilation

Une requête de compilation Overleaf transite `web` $$\rightarrow$$ `clsi` $$\rightarrow$$ un conteneur de compilation. Dans un déploiement à compilations en bac à sable (`SIBLING_CONTAINERS_ENABLED=true`), CLSI n’exécute pas `latexmk` dans le même processus ; il demande au démon Docker de l’hôte, accessible via un socket monté en bind, de démarrer un nouveau conteneur à partir d’une image TeX Live avec le répertoire du projet monté en bind à `/compile`. Une compilation est donc un conteneur de courte durée exécutant un `latexmk` processus.

Trois conséquences s’ensuivent, et les 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 est exactement ce que le runner Docker demande — nous montrons en §6.2 qu’en pratique il ne demande rien. Troisièmement, l’ensemble de travail n’est pas dominé par le document mais par l’arborescence TeX Live, un corpus en lecture seule d’environ 32 Gio que chaque compilation simultanée lit et partage donc via le cache de pages de l’hôte. Ce partage est à l’origine de la mise à l’échelle superlinéaire de la mémoire que nous observons.

### 2.2 Activation des compilations en bac à sable

L’édition communautaire d’Overleaf s’exécute `latexmk` à l’intérieur même du conteneur applicatif. 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 *de l’hôte* démon Docker plutôt qu’imbriqué dans le conteneur applicatif. Deux réglages du toolkit activent cela :

```ini
# config/overleaf.rc
SERVER_PRO=true
SIBLING_CONTAINERS_ENABLED=true
DOCKER_SOCKET_PATH=/var/run/docker.sock

# config/variables.env
TEX_LIVE_DOCKER_IMAGE=ghcr.io/ayaka-notes/texlive-full:2025.1
ALL_TEX_LIVE_DOCKER_IMAGES=ghcr.io/ayaka-notes/texlive-full:2025.1
```

Le toolkit monte en bind le socket Docker de l’hôte dans le conteneur applicatif et traduit cela en variables d’environnement que CLSI lit : `SANDBOXED_COMPILES=true`, `SANDBOXED_COMPILES_SIBLING_CONTAINERS=true`, et `SANDBOXED_COMPILES_HOST_DIR`, le dernier étant le *chemin hôte* du répertoire de compilation. Ce chemin compte : puisque le démon qui démarre le conteneur de compilation est celui de l’hôte, le montage en bind qui lui est donné doit être résolvable dans l’espace de noms de l’hôte, pas dans celui du conteneur applicatif. Le `config/env.sh` du 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}` en cours d’exécution `latexmk` depuis l’image TeX Live, sortant avec 0. C’est l’unité dont nous mesurons la multiplicité tout au long de l’article, et l’absence complète de limites de ressources dont nous rendons compte en §6.2.

**Pourquoi cela compte pour l’étude.**

Les conteneurs frères rendent la mesure propre — chaque compilation est une entité observable, planifiée indépendamment par le système d’exploitation — mais ils signifient aussi que c’est le noyau invité, et non Overleaf, qui arbitre le CPU et la mémoire entre les compilations. Chaque loi de mise à l’échelle dans cet article est donc une propriété du planificateur Linux appliqué à $$N$$ des processus monothread, ce qui explique sa grande régularité.

<figure><img src="/files/34cea18219404e61dc53593200eacf0f05bfd12b" alt=""><figcaption></figcaption></figure>

**Figure 1.** Une requête de compilation, suivie à travers les microservices de l’édition communautaire. La séparation aux étapes et compte pour la capacité : le texte du document est copié dans le corps de la requête, tandis que les ressources binaires sont passées par référence et récupérées par `clsi`. Aucun des deux ne domine — le coût de compilation d’un projet est fixé par l’arborescence TeX Live de 32 Gio que chaque compilation simultanée lit via le cache de pages partagé.

<figure><img src="/files/50d4c3d2c4bf3e9a2f1e4eb1c3f1aed5a8ea0985" alt=""><figcaption></figcaption></figure>

**Figure 2.** Trois topologies de déploiement et l’endroit où le plafond de compilation par instance se situe dans chacune ; les panneaux empilés indiquent la réplication. La constante de 65 compilations protège *un* CLSI, donc la flotte SaaS la multiplie par les instances et les zones (a), et la mise à l’échelle horizontale prise en charge par Server Pro et Ayakaleaf Pro la multiplie par les instances (c) — au prix d’un MongoDB central, de Redis et d’un stockage compatible S3, d’un équilibreur de charge avec affinité de session par cookie (la sortie de compilation est écrite sur le disque local à l’instance, donc une compilation et son téléchargement PDF ultérieur doivent aboutir sur la même instance), et d’un singleton `git-bridge`. Le réglage par défaut du toolkit (b), qui est ce que nous mesurons, a un multiplicateur de un, donc une constante dimensionnée pour un membre de la flotte devient le plafond de l’installation entière.

<figure><img src="/files/c06547c909685ee335e164f926726f34a952440f" alt=""><figcaption></figcaption></figure>

**Figure 3.** Sélection des shards dans `clsi-cache`. Un projet est mappé par $$\operatorname{crc32}(\text{projectId}\text{-}i)\bmod|\text{shards}|$$, 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 basé sur un anneau : faire passer la flotte de trois shards à quatre repartitionne tout l’espace et remappe pratiquement chaque projet (a, b). C’est précisément pourquoi l’implémentation a besoin d’une montée explicite de resharde online, déplaçant sur une fenêtre de temps une fraction de projets croissant linéairement de `currentShards` à `desiredShards` au lieu du $$K/n$$ mouvement qu’offrirait un anneau de hachage cohérent. Lorsque le disjoncteur d’un shard se déclenche, le sel $$i$$ est incrémenté et le shard retiré de la liste des candidats, si bien que la recherche continue au lieu d’échouer (c).

### 2.3 Les deux modes d’échec

Toutes les configurations que nous avons mesurées échouent exactement d’une des deux manières, et la distinction est visible dans le code de réponse plutôt qu’inférée :

* **Épuisement de mémoire** — la pile Overleaf elle-même devient injoignable et la requête renvoie **HTTP 502**. La mémoire invité disponible au niveau d’échec est généralement inférieure à 500 Mio.
* **Délai d’expiration de compilation** — CLSI interrompt la compilation au délai d’expiration par utilisateur et renvoie le statut `délai expiré`. La mémoire disponible au niveau d’échec est souvent de plusieurs gigaoctets.

Nous classons chaque configuration par cette signature plutôt que par une heuristique sur les rapports de ressources, ce qui rend la question « quel mur avons-nous atteint » directement répondable à partir des données elles-mêmes.

## 3. Méthodologie

### 3.1 Banc d’essai et contrôle de l’horloge

Tous les invités s’exécutent sous QEMU/KVM sur un seul 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 le Overleaf Toolkit déployant Ayakaleaf Pro v6.2.2 avec compilations en bac à sable contre `texlive-full:2025.1`.

Un CPU de bureau courant est un mauvais proxy pour un serveur à moins que son horloge ne soit contrôlée. KVM n’offre aucun mécanisme pour fixer une horloge virtuelle : un vCPU est un thread hôte et tourne à la fréquence à laquelle tourne le cœur hôte. 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 en épinglant les vCPU de l’invité aux cœurs P physiques avec `taskset`. La distinction compte sur un CPU à cœurs hybrides : les cœurs E de cette puce ont une horloge de base de 2,4 GHz et *ne peuvent pas* atteindre 3,0 GHz une fois le turbo désactivé, donc un essai qui bascule sur eux mesure silencieusement une machine plus lente. En charge complète, nous vérifions exactement 3000 MHz sur les seize threads épinglés. Un script de garde affirme cet invariant avant chaque benchmark et refuse sinon de démarrer ; il a détecté un réarmement silencieux du gouverneur pendant l’étude.

### 3.2 Un second banc d’essai : un grand runner

La matrice QEMU isole une variable à la fois, mais elle plafonne à seize threads épinglés. Pour vérifier si les mêmes lois tiennent encore avec un ordre de grandeur de plus, nous avons répété le balayage de concurrence sur un seul 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 contre la même `texlive-full:2025.1`. Contrairement aux invités QEMU, cette machine n’est pas verrouillée en fréquence : c’est un serveur de classe production et nous la mesurons telle quelle.

Deux précautions opérationnelles étaient nécessaires et méritent d’être mentionnées, car sans elles l’expérience mesure le harnais plutôt que le serveur. Premièrement, chaque conteneur a été confiné dans une tranche systemd avec `systemd` slice avec `MemoryMax=940 GiB`, de sorte qu’un balayage dérapant épuise un cgroup plutôt que l’hôte. Deuxièmement, les compilations en bac à sable sont créées par le démon hôte et chacune salit 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 — si bien que la racine de données Docker a été déplacée vers un périphérique NVMe dédié. Un balayage à $$N=1024$$ é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 vrai mémoire de master de 63 pages (modèle SJTU) compilé avec XeLaTeX via `latexmk`, contenant des figures TikZ, `biblatex` le traitement bibliographique, et des ressources PDF intégrées — c’est-à-dire une charge réaliste plutôt que synthétique. Une compilation unique sur un invité non chargé prend 8,6–9,8 s sur toutes les configurations, ce que nous utilisons comme base de vol libre $$T\_1$$.

### 3.4 Génération de charge

Nous créons 512 vrais comptes utilisateurs et donnons à chacun sa propre copie du projet, de sorte que les compilations simultanées se disputent exactement comme le feraient des utilisateurs indépendants au lieu 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 invité.

La concurrence est *simultanée*, non étalée. Chaque session est d’abord établie — connexion, jeton CSRF, sélection du compilateur — et seulement ensuite chaque thread dort jusqu’à un instant commun de l’horloge, calculé une fois et partagé, avant d’émettre son `POST /project/:id/compile`. La distinction n’est pas du pinaillage. Une montée étalé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 la même annonce de deadline, ce qui est le cas que les opérateurs craignent 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 devaient être supprimé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, pas un

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 adresse `X-Forwarded-For` distincte supprime 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 à partir de l’indice utilisateur afin que les utilisateurs consécutifs aboutissent dans différents `/24`sous-réseaux,

$$
\texttt{203.};\big\lfloor i/250 \big\rfloor \bmod 100 + 1\texttt{.};
i \bmod 250 + 1\texttt{.}; i \bmod 200 + 10 ,
$$

ce qui laisse les deux limiteurs tranquilles pour l’ensemble complet des 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 respecte `X-Forwarded-For` que pour les pairs qu’on lui a indiqué de faire confiance, et le `trustedProxyIps` est par défaut `loopback`. Comme le générateur de charge atteint l’application via le bridge du conteneur plutôt que par l’interface loopback, l’en-tête est analysé puis jeté, et chaque utilisateur simulé retombe sur une seule adresse. Le symptôme est une vague de `HTTP 429` au vingtième login exactement, ce qui est facile à prendre à tort pour une surcharge serveur. Le réseau de la passerelle doit être ajouté explicitement à la chaîne de confiance ; dans le déploiement clusterisé du §4.3, les CIDR des pods et des services doivent aussi être ajoutés.

#### 3.4.3 Un équilibreur de charge écrasera l’en-tête qu’on lui a demandé de préserver

Lorsque l’instance se trouve derrière un proxy, l’option classique `option forwardfor` *ajoute* la véritable adresse client à la chaîne, ce qui est le bon comportement en production et précisément faux ici : l’adresse synthétique est remplacée par celle du générateur de charge. La directive doit être qualifié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 manque de capacité

À $$N=1024$$ 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 la mise en place de session plutôt que pendant la mesure. L’échec est silencieux : 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 un sous-processus pour compter les conteneurs meurt avec `EMFILE` et tronque silencieusement la télémétrie. La limite souple doit être relevée sur le générateur — la limite dure sur notre hôte était déjà 1048576 — et l’exécution répétée. Nous rapportons les deux exécutions en §4.3 : celle corrigée atteint 1024 sur 1024 avec une médiane à 1,2 s de celle tronquée, raison pour laquelle nous considérons la première comme exploitable mais pas comme 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 Montée en température

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é à l’état stable : la même configuration 2 vCPU / 2 Gio donne 36,5 s à froid et 9,8 s à chaud, soit un facteur 3,7. Chaque configuration effectue donc deux montées en température de compilation unique, jetées après démarrage.

#### 3.5.2 Critère de passage

Un niveau de concurrence ne passe que si *chaque* compilation réussit et si 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 passé une fois avec une médiane de 80,2 s puis a expiré sur les 32 compilations lors de la répétition, donc nous rapportons 31.

#### 3.5.3 Recherche

Les niveaux sont localisés par encadrement exponentiel à partir d’une graine prédite par le modèle, suivi d’une dichotomie entière exacte. Comme le critère est tout ou rien, un niveau est tranché par son premier échec, donc nous abandonnons les requêtes encore en vol dès qu’une seule échoue — sauf aux petits niveaux, où les compilations abandonnées épinglent un petit invité assez fortement pour qu’il ne s’en remette jamais.

#### 3.5.4 Isolation entre niveaux

Les conteneurs de compilation sont vidés, et l’application web est sondée jusqu’à ce qu’elle réponde de nouveau, avant le démarrage du niveau suivant. Sans cela, un niveau suivant un crash enregistre un faux échec à zéro session.

#### 3.5.5 Hygiène de l’hôte

Les autres machines virtuelles sur l’hôte ont été arrêtées : avec 24 Gio de mémoire hôte engagés ailleurs, la même configuration invité signalait 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. Le lire horizontalement est la première surprise. À 4 Gio, les invités à 2, 4 et 8 vCPU atteignent tous exactement 9 — quadrupler les cœurs ne change strictement rien. À 16 Gio, ils atteignent 54, 45 et 57 : passer de 4 à 16 cœurs n’apporte que 6 %, et l’invité 8 cœurs est en fait *pire* que celui à 4 cœurs (§5.2). Ce n’est qu’à 48 Gio que le nombre de cœurs sépare nettement les configurations : 143, 268 et 331.

<figure><img src="/files/729329d72e038ed954de8fd366ece792c1b6d19c" alt=""><figcaption></figcaption></figure>

**Figure 4.** Capacité mesurée sur la matrice de configurations. (a) Chaque configuration sous forme de barre, regroupée par mémoire et colorée par nombre de cœurs ; les barres pleines sont limitées par la mémoire (l’invité meurt d’épuisement mémoire) et les barres hachurées sont limitées par le CPU (les compilations expirent avec de la mémoire en réserve). Lire un groupe de gauche à droite montre combien peu le nombre de cœurs apporte en dessous de 16 Gio ; lire les groupes entre eux montre le rendement superlinéaire de la mémoire. (b) Les mêmes points face au modèle ajusté $$N\_{\max}=\min(0.69R^{1.60},,26.4C)$$; la ligne en tirets est le mur mémoire et les horizontales pointillées sont les plafonds CPU par nombre de cœurs. Une configuration est contrainte par la première des deux qu’elle rencontre.

Lire verticalement une colonne est la seconde : à cœurs fixes, la capacité croît superlinéairement avec la mémoire, à peu près comme $$R^{1.6}$$, pour la raison de cache de pages développée en §5.1.

| mémoire | 2 vCPU |  4 vCPU |  8 vCPU | 16 vCPU |
| ------: | -----: | ------: | ------: | ------: |
|   2 Gio |      1 |       1 |       1 |       — |
|   3 Gio |      4 |       5 |       6 |       — |
|   4 Gio |      9 |       9 |       9 |       — |
|   8 Gio |     21 |      22 |      26 |       — |
|  16 Gio |      — |      54 |  **45** |      57 |
|  32 Gio |      — | **145** |     135 |     141 |
|  48 Gio |      — | **143** | **268** | **331** |

**Tableau 1.** Nombre maximal de compilations simultanées qui aboutissent avec succès, mesuré avec un délai d’expiration de compilation de 300 s et le plafond de concurrence de CLSI levé. **Gras** marque une configuration limitée par le CPU (les compilations expirent alors que de la mémoire reste disponible) ; le reste est limité par la mémoire (la pile meurt avec HTTP 502). La ligne 2 Gio porte la correction discutée en §5.2.

### 4.2 La concurrence est un partage du temps

La figure 5 balaie tous les niveaux 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 à $$N=1$$ à 9,1 s à $$N=C=8$$, soit une variation de 5 %. Au-dessus, le temps croît strictement proportionnellement à $$N/C$$ : à $$N=16,24$$ nous mesurons 18,5 s et 27,1 s, soit un ratio de $$1:2.13:3.12$$ par rapport à un idéal $$1:2:3$$.

<figure><img src="/files/a7dd87b3f5a5ef31546a7617e0c31c258e1424e7" alt=""><figcaption></figcaption></figure>

**Figure 5.** Latence de compilation en fonction de la concurrence, à matériel fixe. Le coude est à $$N=C$$ ; au-delà, le ralentissement mesuré suit $$N/C$$ à 5–7 % près. Les quinze niveaux ont tous abouti.

<figure><img src="/files/f6b9d4a210232a2da5eb020b1f7ffaee879e9eb4" alt=""><figcaption></figcaption></figure>

**Figure 6.** Latence de compilation en fonction de la concurrence pour plusieurs configurations. Chaque panneau garde le matériel fixe et balaie la charge offerte ; la règle verticale marque $$N=C$$. Les courbes sont plates à gauche de celle-ci et linéaires en $$N/C$$ à droite, ce qui est la signature d’un partage du temps plutôt que d’une contention : le travail ne devient pas plus coûteux, il attend simplement son tour.

L’ajustement $$T(N)=T\_1\max(1,N/C)^{b}$$ sur l’ensemble des mesures réussies de l’étude donne $$b=0.914$$ ($$R^2\_{\log}=0.904$$, $$n=81$$). Un exposant indiscernable de l’unité est l’énoncé quantitatif qu’une compilation est une unité de travail monothread et limitée par le CPU, et que la concurrence n’aide ni ne nuit au-delà du partage des cœurs. Le corollaire pratique est inconfortable pour le dimensionnement de capacité : une configuration peut absorber un nombre arbitraire d’utilisateurs sans *échouer* tout en faisant attendre chacun proportionnellement plus longtemps. À $$N=56$$ sur cet invité, toutes les compilations réussissent encore, mais chaque utilisateur attend 64,8 s au lieu de 8,7 s.

### 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 via `DELETE /project/:id/output`, de sorte qu’aucun niveau ne bénéficie du travail effectué par le niveau inférieur. La base de référence à une compilation sur cette machine est de 28,8 s, ce qui est la valeur à froid et ne doit pas être comparée à la base à l’état stable de 8,6–9,8 s utilisée plus haut ; la base à froid sur les invités QEMU est de 28,3 s, donc par thread les deux machines sont à deux pour cent près l’une de l’autre pour cette charge de travail.

| $$N$$ |    succès | $$p\_{50}$$ | $$p\_{95}$$ | $$p\_{50}/T\_1$$ | pics de conteneurs |
| ----: | --------: | ----------: | ----------: | ---------------: | -----------------: |
|    64 |     64/64 |      55,0 s |      55,1 s |             1,9× |                  — |
|   128 |   128/128 |      73,6 s |      74,4 s |             2,6× |                  — |
|   192 |   192/192 |      80,2 s |      87,3 s |             2,8× |                  — |
|   256 |   256/256 |      69,8 s |     121,2 s |             2,4× |                151 |
|   512 |   512/512 |     179,2 s |     344,6 s |             6,2× |                205 |
|  1024 | 1024/1024 |     280,2 s |     468,9 s |             9,7× |                179 |

**Tableau 2.** Balayage de concurrence sur un EPYC 7773X (64 cœurs / 128 threads, 995 Gio). Tous les niveaux à froid ; base 28,8 s. Les pics de conteneurs désignent le nombre maximal de bacs à sable vivants simultanément.

<figure><img src="/files/a8723c3b3ad859fdd0207e9489d660fc20ea09ba" alt=""><figcaption></figcaption></figure>

**Figure 7.** Mise à l’échelle verticale sur un grand runner. (a) Latence en fonction de la concurrence offerte ; la zone ombrée marque le régime au-delà du coude. (b) Le nombre de bacs à sable réellement vivants ne suit jamais le nombre demandé — il se sature près de 200 — tandis que le cgroup de compilation n’utilise jamais plus d’un cinquième de son plafond.

#### 4.3.1 La machine ne tombe jamais en panne

Chaque niveau se termine à 100 %, y compris $$N=1024$$ — huit fois le nombre de threads. Nous n’avons pas trouvé le plafond de capacité de cette machine ; nous avons manqué de patience avant qu’elle ne manque de marge. C’est la première configuration de l’étude où la contrainte limitante n’est pas la mémoire : à $$N=1024$$ le cgroup de compilation culmine à 184 Gio, soit un cinquième de son plafond de 940 Gio, tandis que le CPU est à 100 % d’utilisation avec une charge moyenne de 166.

#### 4.3.2 La dégradation est sous-linéaire parce que l’admission est limitée en débit

Le partage du temps naïf prédit que $$8\times$$ les threads coûte $$8\times$$ la latence. $$9.7\times$$ par rapport à une compilation unique, mais seulement $$3.8\times$$ par rapport à $$N=128$$ — pour un accroissement par huit de la charge offerte. La raison est visible dans 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 bacs à sable réellement vivants 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 font la queue à l’admission au lieu de se contendre à l’intérieur du CPU. C’est cette mise en file qui sauve la queue ici, et elle le fait par accident.

#### 4.3.3 Le coude est à 512, pas au point de défaillance

Entre $$N=256$$ et $$N=512$$ la $$p\_{95}$$ latence augmente de $$2.9\times$$ pour un doublement de charge ; chaque doublement antérieur coûtait entre $$1.2\times$$ et $$1.4\times$$. Une capacité formulée comme « la plus grande $$N$$ concurrence qui ne tombe pas en échec » rapporterait 1024 et serait inutilisable pour un opérateur : à ce point, l’attente en queue est de près de huit minutes.

### 4.4 Le temps de compilation est inversement proportionnel à l’horloge

Puisque la charge est limitée par le CPU, son coût devrait évoluer comme $$1/f$$. Nous le testons directement en balayant l’horloge de l’hôte sur toute la plage de la machine, 1,0–5,5 GHz en dix étapes, sur un invité par ailleurs inchangé (figure 8). Le temps d’une compilation unique passe de 26,5 s à 4,8 s : une horloge 5,5× plus rapide procure une accélération 5,5× plus grande sans aucun rendement décroissant sur toute la plage. Le produit $$T!\cdot!f$$ est constant à 2 % près sur les dix fréquences.

La normalisation par la part de cœur fait s’effondrer les trente mesures — trois niveaux de concurrence à dix fréquences — sur une seule constante :

$$
T(N,f) ;=; \frac{k}{f},\max!\left(1,\frac{N}{C}\right),
\qquad k = 27.9\ \mathrm{GHz\cdot s}
$$

avec une dispersion résiduelle de 5,1 % sur une plage où l’horloge elle-même varie d’un facteur 5,5×. L’absence de toute courbure est elle-même le résultat : si la charge avait été limitée par la bande passante mémoire ou par les E/S, $$T$$ s’aplatirait à haute fréquence à mesure que le CPU dépasserait l’autre ressource.

<figure><img src="/files/78e7ed538d6d35a16b1e5e8618080d1ce8ca8eee" alt=""><figcaption></figcaption></figure>

**Figure 8.** Balayage de l’horloge. (a) $$T=k/f$$ avec l’hyperbole ajustée. (b) Après division par $$\max(1,N/C)$$ tous les points se superposent sur une seule constante, ce qui confirme l’équation (1).

L’équation (1) a une conséquence d’approvisionnement directe, facile à énoncer et facile à mal interpréter : *l’horloge améliore l’expérience de chaque utilisateur individuel, le nombre de cœurs ne fait qu’en admettre davantage*. Une machine avec une horloge 20 % plus élevée compile 20 % plus vite pour tout le monde, sans rendement décroissant ; deux fois plus de cœurs n’accélèrent la compilation de personne du tout.

## 5. Analyse

### 5.1 Deux murs, ajustés séparément

Chaque configuration est classée par sa signature d’échec (§2.3), puis le mur mémoire et le plafond CPU sont ajustés uniquement sur les configurations qui les atteignent réellement :

$$
N\_{\max} = \min\left(A R^{p},; k\_c C\right)
$$

avec $$R$$ en gibioctets et $$C$$ en vCPU.

L'exposant de la muraille mémoire est constamment supralinéaire, $$p>1$$: le coût mémoire marginal d'une compilation concurrente supplémentaire *diminue* à mesure que la mémoire totale augmente, d'environ 312 MiB par compilation sur un invité de 3 GiB à environ 194 MiB sur un de 32 GiB. Le mécanisme est le cache de pages partagé sur l'arborescence TeX Live décrit en §2.1 : les compilations concurrentes lisent des fichiers de polices et de macros qui se recoupent, de sorte 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.

<figure><img src="/files/be82a9412a96ec2f1395f99dacddb0bc7b069256" alt=""><figcaption></figcaption></figure>

**Figure 9.** Les mêmes données sous forme de deux surfaces sur le $$(C,R)$$ plan. (a) Capacité : la surface ajustée est une crête, pas un plan — elle grimpe fortement avec la mémoire et reste presque plate le long de l'axe des cœurs jusqu'à ce que la mémoire cesse d'être le facteur limitant, ce qui explique pourquoi la ligne 48 GiB est la seule où le nombre de cœurs distingue les configurations. (b) Latence en fonction de la concurrence pour chaque configuration, avec la surface ajustée $$T=12.6,(N/C)^{0.91}$$ montrée en pointillés et le délai d'attente de 180 s tracé comme un plan. Une configuration échoue lorsque sa courbe pleine perce ce plan, ce qui montre clairement à quel point le réglage du délai d'attente détermine directement la capacité rapportée.

### 5.2 Quand davantage de cœurs aggravent les choses

L'équation (2) est un minimum de deux termes et donc monotone en $$C$$, mais les mesures ne le sont pas. Nous observons deux inversions dans lesquelles l'ajout de cœurs *réduit* la capacité : à 16 GiB (54 contre 45) et à 32 GiB (145 contre 135). Les deux se produisent dans le régime limité par la mémoire, et le mécanisme est le même dans chacun des cas : avec davantage de cœurs, les compilations concurrentes avancent de concert et atteignent leur taille résidente maximale au même moment, alors qu'avec moins de cœurs l'ordonnanceur les entremêle et les pics sont décalés. Sur un invité dont la marge mémoire est déjà marginale, le décalage est ce qui le maintient en vie. Un modèle de capacité fondé sur l'utilisation moyenne des ressources ne peut pas exprimer cela ; c'est une propriété de la *coïncidence* des pics.

Une troisième inversion apparente, à 2 GiB, nous l'écartons désormais. L'enregistrement de recherche indique une capacité de 2 à 2 vCPU mais de 1 à 4 et 8 vCPU, ce qui semble relever du même effet. La relecture des balayages bruts montre quelque chose de plus simple : à 2 GiB, le niveau $$N=2$$ réussissait du premier coup pour les trois nombres de cœurs puis échouait à sa run de confirmation pour deux des trois. Le niveau n'est pas une capacité mais un pile ou face, et l'entrée à 2 vCPU est le lancer tombé du bon côté. Nous rapportons donc la valeur reproductible, 1, pour les trois nombres de cœurs et n'en tirons aucune conclusion. Nous consignons ici la correction plutôt que de reformuler silencieusement le tableau, parce que la lecture écartée est précisément celle qui aurait soutenu une revendication intéressante.

## 6. Résultats d'implémentation

### 6.1 Une limite de concurrence codée en dur

Sur des invités suffisamment grands, la capacité s'est arrêtée exactement à 65 compilations simultanées, quelle que soit la concurrence demandée : à $$N=66,80,96,128$$ nous avons mesuré $$65$$ des succès et $$1,15,31,63$$ des réponses `immédiates` indisponibles

La cause est une constante dans CLSI :

```javascript
// services/clsi/config/settings.defaults.cjs:110
compileConcurrencyLimit: isSpotInstance ? 32 : 64,

// services/clsi/app/js/LockManager.js
if (LOCKS.size <= Settings.compileConcurrencyLimit) return   // <= admet 64+1
throw new Errors.TooManyCompileRequestsError(...)
```

La comparaison n'est pas stricte, donc le plafond effectif est $$64+1=65$$, ce qui correspond exactement à la mesure. Les requêtes excédentaires reçoivent **HTTP 503**  — elles sont *rejetées*, pas mises en file, donc du point de vue de l'utilisateur le bouton de compilation échoue simplement. Contrairement à tous les autres réglages modulables du même fichier, celui-ci ne lit aucune variable d'environnement ; il a été introduit en amont en août 2024 et ne peut être modifié qu'en changeant l'image. Avec la limite relevée, le même invité 16 vCPU / 32 GiB qui signalait `succès=65, indisponibles=15` à $$N=80$$ signalait à la place `succès=80`.

### 6.2 Une limite de mémoire de conteneur inopérante

Inspecter un conteneur de compilation en cours d'exécution ne montre absolument aucune isolation des ressources :

```
Memory=0  NanoCpus=0  CpuShares=0  CpuQuota=0  CpusetCpus=[]
Ulimits=[{Name:cpu Soft:305 Hard:310}]
```

L'absence de toute quota CPU est intentionnelle et explique pourquoi l'exposant de partage du temps du §4.2 est si net : rien ne déforme la compétition entre compilations. L'absence d'une *mémoire* limite, en revanche, n'est pas intentionnelle. Le lanceur Docker en demande bien une :

```javascript
// services/clsi/app/js/DockerRunner.mjs:262
Memory: 1024 * 1024 * 1024 * 1024, // 1 Go
```

Ceci est faux à deux égards. La valeur est $$1024^4=1 tebiB$$ là où le commentaire entend $$1024^3$$; et le champ est placé au niveau supérieur des options de création plutôt qu'à l'intérieur de `HostConfig`, là où l'API Docker l'attend, donc il est ignoré — ce que confirme le `Memory=0` . 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, dont aucune ne revisite la sémantique. Notamment `MAX_OUTPUT = 1024 * 1024 // 1 Mo` dans le même commit est correcte, ce qui indique une erreur d'inattention plutôt qu'un malentendu.

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 Docker qui termine un conteneur fautif ; il fait tomber tout l'invité. Sur la configuration 2 vCPU / 2 GiB, 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é se redémarrant finalement tout seul. Une limite par conteneur fonctionnelle se dégraderait bien plus gracieusement : la compilation trop volumineuse échouerait et le service survivrait.

La seule limite qui s'applique est `RLIMIT_CPU`, réglée à $$\text{timeout}+5$$ secondes. Elle borne le *temps CPU* , pas le temps écoulé, et une seule compilation ne consomme qu'environ 9 s de CPU, donc elle ne s'applique jamais à aucune concurrence ; elle protège contre des entrées pathologiques comme une macro folle. C'est toutefois un oracle utile : observer `Soft:305` confirme qu'un réglage de délai d'attente de 300 s a bien été propagé au conteneur.

### 6.3 Le délai d'attente de compilation est le réglage dominant

Le champ par utilisateur `features.compileTimeout` est par défaut de 180 s. Pour toute configuration limitée par le CPU, ce n'est pas une marge de sécurité mais un réglage de capacité, parce qu'une machine qui calcule encore correctement est déclarée en échec. Le relever à 300 s — une simple mise à jour MongoDB — modifie la capacité mesurée d'un facteur `RequestParser.MAX_TIMEOUT`au-delà duquel la valeur est tronquée silencieusement.

| Configuration    | 180 s | 300 s |        Rapport | Contrainte limitante                  |
| ---------------- | ----: | ----: | -------------: | ------------------------------------- |
| 8 vCPU / 48 GiB  |    64 |   268 | $$4.19\times$$ | CPU, 28 GiB libres                    |
| 4 vCPU / 32 GiB  |    47 |   145 | $$3.09\times$$ | CPU, 30 GiB libres                    |
| 4 vCPU / 48 GiB  |    63 |   143 | $$2.27\times$$ | temps CPU                             |
| 4 vCPU / 16 GiB  |    31 |    54 | $$1.74\times$$ | temps CPU                             |
| 2 vCPU / 8 GiB   |    15 |    21 | $$1.40\times$$ | temps CPU                             |
| 8 vCPU / 32 GiB  |   127 |   135 | $$1.06\times$$ | rencontre ensuite la muraille mémoire |
| 8 vCPU / 16 GiB  |    56 |    45 | $$0.80\times$$ | mémoire                               |
| 16 vCPU / 32 GiB |   159 |   141 | $$0.89\times$$ | mémoire                               |

**Tableau 3.** Effet du délai d'attente 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 délai d'attente unique. Pour *mémoire*-limitées par le CPU, un délai d'attente plus long *réduit* augmente la capacité, parce que chaque compilation conserve son ensemble résidant plus longtemps et que davantage d'entre elles se chevauchent. Une valeur de capacité est donc dénuée de sens sans préciser le délai d'attente dans lequel elle a été mesurée, et on ne peut pas mélanger les deux dans un même tableau.

## 7. Travaux connexes

### 7.1 Recommandations du fournisseur

La documentation matérielle d'Overleaf elle-même énonce les faits qualitatifs que nous quantifions ici : que LaTeX est à thread unique, que la performance d'un seul cœur gouverne donc le temps de compilation, et que « davantage de cœurs n'aideront que si vous essayez de compiler plus de documents que vous n'avez de cœurs CPU libres » \[1]. Elle donne ensuite la règle de dimensionnement linéaire — une base de 2 cœurs/3 GiB plus un cœur et un gigaoctet par cinq à dix utilisateurs concurrents — 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 se brise : elle ne comporte aucun terme pour le cache de pages partagé qui rend la muraille mémoire supralinéaire, ni aucun terme pour les deux paramètres logiciels qui dominent le résultat.

### 7.2 Études de capacité des builds et de la CI

La mesure des systèmes de build sous concurrence est bien établie hors du contexte LaTeX. LightSys rapporte que les systèmes de CI conventionnels compilant dans des conteneurs Docker se dégradent en I/O à mesure que le taux d'arrivée des pull requests augmente, avec un goulot d'étranglement apparaissant autour de onze requêtes concurrentes \[17] ; TAOS-CI observe que la compilation domine le temps de bout en bout de la CI, représentant 60–67 % de la durée totale du pipeline sur de grands projets \[18]. Notre système diffère sur un point qui se révèle décisif : une compilation LaTeX est interactive. Un job CI qui prend deux fois plus de temps est une gêne ; une compilation qui prend deux fois plus de temps est directement perçue par un utilisateur qui attend dans un volet d'aperçu, c'est pourquoi nous traitons le délai d'attente non comme un seuil d'échec mais comme un paramètre de capacité.

### 7.3 Surcharges 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 d'un conteneur par compilation est amorti : c'est une petite constante par rapport à une compilation de 9 s, et le temps de vol $$T\_1$$ que nous ajustons l'absorbe. La propriété du conteneur qui compte est l' *absence* de limites de ressources (§6.2), qui transforme un dépassement mémoire par compilation en panne de l'hôte entier.

### 7.4 LaTeX comme entrée non fiable

La compilation en bac à sable 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é dont le comportement en matière de ressources est observable — et aussi ce qui rend l'absence de limite mémoire conséquente, puisque les opérateurs qui la déploient supposent cette isolation.

### 7.5 Le compilateur comme objet d'étude

TeX lui-même est bien documenté en tant que langage \[16], mais son comportement comme *cible de build* n'a suscité l'attention que récemment. Tan et Rigger \[8] compilent un vaste corpus de sources arXiv à travers les moteurs et les versions de distributions et constatent que le choix du moteur n'est pas substituable : seule une fraction de pour cent des documents produit une sortie octet à octet identique sous XeTeX et pdfTeX. Ce résultat a une incidence directe sur 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 épinglons donc un document, un moteur et une distribution (`texlive-full:2025.1`) tout au long du papier, et nous indiquons le moteur dans la légende de chaque figure. Cela borne aussi la généralité de nos chiffres d'une manière qu'il vaut la peine d'énoncer clairement : ils caractérisent XeLaTeX sur ce document, et non TeX en général.

Les travaux sur les *systèmes* de build LaTeX sont en grande partie pilotés par les praticiens. Le projet LaTeX3 `l3build` \[13] standardise les tests de régression et la mise en paquet, et des benchmarks indépendants comparent des outils enveloppes — une enquête sur 26 systèmes de build trouve qu'un préambule précompilé vaut environ 20 % de mieux qu'un exécutable simple et 40 % de mieux que `latexmk` \[14]. Ceux-ci optimisent la compilation *unique* . Ils sont orthogonaux à ce que nous mesurons, et se composent avec : un cache de préambule raccourcit $$T\_1$$, et toutes les valeurs de capacité de cet article évoluent avec $$T\_1$$.

### 7.6 Contrôle de la concurrence dans l'éditeur, pas dans le compilateur

La moitié collaborative d'Overleaf repose sur une ligne de travail bien établie. La transformation opérationnelle trouve son origine chez Ellis et Gibbs \[9] et a été rendue pratique pour les clients à forte latence par le système Jupiter \[10], dont la conception est reconnaissable dans `document-updater`: un serveur qui ordonne les opérations et un tampon par document auquel les clients se synchronisent. Les types de données répliquées sans conflit \[11] résolvent le même problème sans séquenceur central. Cette distinction est ce qui fait fonctionner la topologie du §4.3 : comme le tampon des mises à jour en attente réside dans Redis partagé plutôt que dans la mémoire d'une instance, une compilation acheminée vers n'importe quelle réplique observe les dernières frappes, et l'affinité de compilation peut être choisie pour la localité du cache plutôt que pour la correction.

### 7.7 Modèles de capacité

La loi d'Amdahl \[24] borne l'accélération due au parallélisme et la loi de Little \[23] relie l'occupation au taux d'arrivée et au temps de service ; les deux sont utilisées ci-dessus. La loi universelle de scalabilité de Gunther \[12] prolonge 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écroît. Nous notons que notre système n' *pas* présente pas ce régime rétrograde jusqu'à $$N=1024$$: le débit se sature et la latence augmente, mais rien ne s'effondre. La raison est structurelle plutôt que chanceuse — les compilations ne partagent aucun état à rendre cohérent, donc 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

{% stepper %}
{% step %}

## Corrigez les deux paramètres logiciels avant d'acheter du matériel

Les deux sont gratuits et les deux valent plus que n'importe quelle amélioration matérielle unique que nous avons mesurée. Augmentez `features.compileTimeout` à une valeur que vos utilisateurs toléreront réellement — le maximum accepté par CLSI est de 600 s — et, si vous vous attendez à dépasser 65 compilations simultanées, relevez soit `compileConcurrencyLimit` dans une image dérivée, soit mettez à l'échelle horizontale. Ne faire ni l'un ni l'autre revient à payer pour des cœurs que le logiciel refuse d'utiliser.
{% endstep %}

{% step %}

## Dimensionnez une machine par son coude, pas par son plafond

Le balayage du grand serveur (§4.3) distingue deux nombres qui sont systématiquement confondus. Le *plafond* — la plus grande concurrence qui renvoie encore tous les PDF — est d'au moins 1024 sur un serveur à 64 cœurs, et nous ne l'avons jamais atteint. Le *coude* — le point au-delà duquel la latence de queue cesse d'augmenter doucement et commence à doubler — est à 512, et le dernier point d'exploitation confortable en dessous est 256. Entre $$N=256$$ et $$N=512$$ la $$p\_{95}$$ l'attente passe de deux minutes à près de six ; entre 512 et 1024, elle atteint huit. Un opérateur qui dimensionne au plafond déploie un système qui fonctionne techniquement et que personne ne veut utiliser.

Pour cette machine et ce document, le point d'exploitation recommandé est donc **256 compilations concurrentes**, ce qui correspond $$4\times$$ au nombre de cœurs physiques et $$2\times$$ au nombre de threads, et qui se maintient $$p\_{95}$$ autour de 120 s. Nous suggérons de fixer `compileConcurrencyLimit` à cette valeur plutôt que de la laisser haute : admettre 1024 compilations d'un coup fait attendre tout le monde huit minutes, alors qu'admettre 256 et mettre le reste en file de traitement sert la plupart des utilisateurs en deux minutes. La mise en file pénalise les arrivées tardives ; la contention pénalise tout le monde.
{% endstep %}

{% step %}

## Considérez ces chiffres comme des pires cas

Chaque niveau du Tableau 2 est une compilation à froid déclenchée simultanément. Aucune de ces conditions n'est vraie en production : une compilation à chaud du même document prend 8,6 s contre 28,3 s à froid, soit un facteur de $$3.3$$, et les vrais utilisateurs n'appuient pas sur le bouton à la même seconde. Une population en régime permanent qui recompile toutes les deux minutes avec un taux de réussite de cache typique pourra donc soutenir beaucoup plus de rédacteurs que le seul nombre de concurrentes ne le suggère — de l'ordre d'un millier ou plus d'auteurs actifs au point d'exploitation 256. Le chiffre de concurrence est une borne sur le pic instantané, pas un nombre de sièges.
{% endstep %}

{% step %}

## Décidez d'abord du budget de latence, puis lisez la taille

L'équation (1) s'inverse directement. Pour une attente cible $$T$$ à l'horloge $$f$$ sur $$C$$ cœurs, la concurrence qui tient est $$N \le C,fT/k$$ avec $$k\approx28 GHz·s$$ pour ce document. Un budget de 60 s sur 8 cœurs à 3 GHz donne $$N\le51$$; un budget de 120 s le double. Publier le budget en même temps que la capacité est la seule façon honnête d'énoncer l'un ou l'autre.
{% endstep %}

{% step %}

## Achetez d'abord de la mémoire, puis des cœurs, et vérifiez sur quelle muraille vous vous trouvez

En dessous de 32 GiB, nous avons mesuré presque aucun bénéfice à ajouter des cœurs. Le diagnostic est simple : si les échecs apparaissent sous forme de HTTP 502 avec l'invité à court de mémoire, ajoutez de la mémoire ; s'ils apparaissent sous forme de `délai expiré` avec de la mémoire de réserve, ajoutez des cœurs ou augmentez le délai d'attente. Les opérateurs peuvent le lire à partir de la même signature d'échec que celle que nous avons utilisée pour classer les configurations.
{% endstep %}

{% step %}

## Privilégiez l'horloge pour l'expérience, les cœurs pour la population

Parce que $$T\propto 1/f$$ tient sans courbure (2 % entre 1,0 et 5,5 GHz), une horloge plus rapide accélère chaque compilation pour chaque utilisateur. Davantage de cœurs n'accélèrent aucune compilation individuelle ; ils admettent seulement davantage de compilations concurrentes. Les déploiements dont la plainte est « les compilations sont lentes » devraient acheter de l'horloge ; ceux dont la plainte est « les compilations échouent à l'échéance » devraient acheter de la mémoire et des cœurs.
{% endstep %}

{% step %}

## Passez à l'échelle horizontale plutôt qu'en hauteur au-delà du plafond

Au-delà de 65 compilations concurrentes, la voie prise en charge est la mise à l'échelle horizontale (Figures 2c et 10, développées au §9) : plusieurs instances d'application derrière un répartiteur de charge avec affinité de session par cookie, partageant MongoDB central, Redis et un stockage compatible S3, avec `git-bridge` laissant l'instance singleton.
{% endstep %}

{% step %}

## Ne comptez pas sur l'isolation par compilation

Jusqu'à ce que la limite mémoire du lanceur Docker soit 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 d'attendre qu'elle existe. Le mécanisme que nous avons utilisé sur le grand hôte est une tranche systemd portant un plafond dur, vers laquelle le démon Docker est ensuite pointé de sorte que chaque conteneur qu'il crée y soit comptabilisé :

```ini
[Slice]
MemoryMax=940G
```

Un détail ici vous coûte un après-midi si vous le ratez. Une tranche nommée `docker-capped.slice` ne se place pas à côté de `docker.slice`; elle se place *à l'intérieur* de celui-ci, car le tiret est le séparateur hiérarchique et non une partie du nom. Un plafond qui semble n'avoir aucun effet a généralement été appliqué un niveau plus loin que là où vivent réellement les conteneurs. Vérifiez en relisant le pic à partir de `memory.max_usage_in_bytes` après une exécution plutôt qu'en faisant confiance 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 est en soi la preuve que le facteur limitant était le démon et non la mémoire.
{% endstep %}
{% endstepper %}

<figure><img src="/files/6d7b54a9f88bbc137b6dd8c3bb20bd5d4cad7f2b" alt=""><figcaption></figcaption></figure>

**Figure 10.** Topologie de référence pour un déploiement à mise à l'échelle horizontale, tirée de la configuration que nous avons vérifiée. Les répliques d'application sont interchangeables et ne conservent rien de durable, elles peuvent donc être ajoutées et supprimées librement. Trois composants ne le sont pas : Redis, dont le tampon de documents est ce qui permet à une compilation acheminée vers n'importe quelle réplique de voir les dernières frappes ; le magasin d'objets, qui devient obligatoire plutôt qu'optionnel à partir de plus d'une réplique ; et `git-bridge`, qui conserve les dépôts sur le disque local sans chemin de réplication et doit fonctionner en singleton à côté d'une réplique désignée.

## 9. Un déploiement de référence multi-machine

Tout ce qui précède mesure une seule machine. Cette section énonce la forme distribuée avec suffisamment de détails pour la construire, et — parce que la question qu'un opérateur se pose réellement n'est pas *comment* mais *si* — énonce d'abord le point à partir duquel l'effort en vaut la peine.

### 9.1 Quand la forme distribuée est justifiée

Une seule machine est moins coûteuse à exploiter sur tous les plans qui comptent : un seul domaine de panne, aucun état partagé à maintenir cohérent, aucun routage à se tromper. Nos données placent trois seuils sur le moment où il faut y renoncer.

#### 9.1.1 En dessous de 65 compilations concurrentes, ne

Le plafond par instance est une constante logicielle, pas matérielle (§6.1). Tant que la charge offerte ne s'en approche pas, une deuxième machine ajoute des modes de défaillance et n'apporte rien. L'hôte à 64 cœurs servait 256 compilations concurrentes avec succès total seulement après que `compileConcurrencyLimit` ait été relevé ; un opérateur qui n'a pas encore modifié cette unique valeur n'est pas contraint par le matériel et ne devrait pas faire ses achats en fonction du matériel.

#### 9.1.2 Entre 65 et environ 500, commencez par monter en puissance

La montée en puissance verticale est restée linéaire sur toute notre plage et n'est jamais entrée dans un régime rétrograde. Un grand hôte a atteint 1024 compilations à froid simultanées avec 100 % de succès (§4.3) ; le coude de latence est apparu à 512, pas avant. Dans cette bande, une machine plus grande est nettement plus simple que plusieurs plus petites et, conformément au §4.4, une machine plus rapide améliore l'expérience de chaque utilisateur plutôt que de simplement en admettre davantage.

#### 9.1.3 Passez au distribué pour la disponibilité, pas pour le débit

La raison honnête d'exécuter plus d'une réplique d'application sous le plafond est qu'une machine, c'est une alimentation, un noyau, une fenêtre de mise à jour. C'est une raison légitime et celle que nous donnerions ; ce n'est tout simplement pas un argument de capacité, et confondre les deux conduit les opérateurs à acheter des répliques alors qu'ils avaient besoin de mémoire.

### 9.2 Niveaux et dimensionnement

La Figure 10 montre la topologie. Elle comporte quatre niveaux, et ils évoluent selon des quantités différentes — ce qui est tout le point de leur séparation.

#### 9.2.1 Bord

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. Sa configuration, et non sa taille, est ce qui compte (§9.3).

#### 9.2.2 Répliques d'application

Celles-ci supportent la charge de compilation et sont le seul niveau qui évolue avec la concurrence. Dimensionnez chacune d'elles selon les règles du §8 — mémoire avant cœurs, puis horloge — puis fixez le nombre de répliques pour couvrir la concurrence de pointe divisée par le plafond par réplique. Les répliques 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 les rend sûres à ajouter et à retirer librement, et il vaut la peine de le vérifier plutôt que de le supposer, car un seul `filestore` mal configuré convertit silencieusement le niveau en un niveau à état.

#### 9.2.3 État

Redis, MongoDB et un magasin d'objets compatible S3, sur des hôtes séparés. Redis est le composant porteur et le moins évident : il contient le magasin de sessions et le tampon de documents en direct, ce qui permet à une compilation acheminée vers n'importe quelle réplique d'observer les frappes saisies sur une autre réplique. Un opérateur qui traite Redis comme un cache et le dimensionne pour l'éviction produira des compilations de documents obsolètes, 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 taux de compilation. Le magasin d'objets est optionnel à une réplique et obligatoire au-delà.

#### 9.2.4 Le singleton

`git-bridge` conserve les dépôts sur le disque local, maintient un index local, et n'a aucun chemin de réplication. Il doit fonctionner comme une seule instance, épinglée à côté d'une réplique désignée, et c'est le composant qui fait que le déploiement n'est pas tout à fait sans état. Planifiez son hôte en conséquence : c'est son disque qu'il faut sauvegarder.

| **Niveau**            | **Nombre**           | **Évolue avec**                    |
| --------------------- | -------------------- | ---------------------------------- |
| Répartiteur de charge | 1–2                  | connexions simultanées             |
| Application           | $$\lceil N/C\rceil$$ | compilations simultanées de pointe |
| Redis                 | 1 (+réplique)        | sessions d'édition actives         |
| MongoDB               | 1 (+réplique)        | projets stockés                    |
| Magasin d'objets      | 1 cluster            | octets totaux des projets          |
| `git-bridge`          | 1 exactement         | dépôts sur disque                  |

**Tableau 4.** Niveaux de référence. Seul le niveau d'application évolue avec la concurrence ; son dimensionnement fait l'objet du §8.

### 9.3 Le routage est la partie qu'il est facile de se tromper

Trois classes de requêtes doivent atteindre trois endroits différents, et la configuration par défaut à règle unique en satisfait au plus 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 avec une seule réplique. Nous utilisons `balance hash path,field(3,/)`\
&#x20;avec `hash-type consistent` et `hash-balance-factor 150`. Le choix compte lors du passage à l'échelle : avec l'affinité par cookie, les sessions existantes restent indéfiniment liées à leur réplique d'origine et une réplique nouvellement ajoutée ne reçoit que de nouveaux utilisateurs, de sorte que la machine que l'opérateur vient d'acheter n'absorbe aucune de la charge qui a motivé l'achat. Le hachage cohérent a redistribué 35 % des projets lors du passage à l'échelle dans notre configuration, contre 0 % pour les cookies.

Le trafic de session est différent. Lorsque la mise à niveau WebSocket échoue et `socket.io` revient au sondage XHR, les sondages successifs d'une même session doivent atteindre une seule réplique, et il n'y a aucun identifiant de projet dans le chemin à hacher. Ce trafic nécessite un backend distinct avec affinité par cookie. Nous avons conçu ce découpage mais ne l'avons pas déployé ; nous le signalons comme un manque plutôt que de le revendiquer.

Enfin, `/git/` doit atteindre la réplique à côté de laquelle `git-bridge` s'exécute. Il est acheminé vers cette réplique plutôt que vers `git-bridge` directement parce que le pont authentifie ses rappels auprès des points de terminaison OAuth de l'application et résout les URL de blobs à travers elle ; contourner la réplique casse l'authentification au lieu d'améliorer quoi que ce soit.

### 9.4 La réduction de capacité nécessite un tampon de vidange

Retirer une réplique n'est pas symétrique à en ajouter une : une compilation en cours est perdue, et l'utilisateur voit un échec qu'il n'a pas provoqué. La séquence exploitable consiste à arrêter d'abord le nouveau trafic, attendre, puis seulement ensuite terminer. Nous avons implémenté cela sous forme de hook pre-stop retenant le pod pendant un intervalle configurable tandis que le répartiteur marque le backend en vidange — 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. L'intervalle est le réglage qui décide si l'élasticité est invisible ou exaspérante.

Une contrainte supplémentaire que nous avons découverte par la mesure plutôt que par la conception : l'autoscaling sur le CPU ne fonctionne pas pour cette charge de travail. L'utilisation propre au pod de l'application est restée à 22 m core contre un total de nœud de 3997 m core, parce que le travail de compilation se déroule dans des conteneurs frères que le pod ne comptabilise pas. Tout signal utilisé pour mettre à l'échelle ce niveau doit compter les conteneurs de compilation en cours d'exécution, pas le CPU du pod.

## 10. Implications au-delà d'Overleaf

Rien dans le §4.2 ou le §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 à thread unique, 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 se transposent directement à quiconque construit un tel service.

### 10.1 Fournissez de la mémoire, puis des cœurs

Le résultat le plus fort de la matrice est négatif : en dessous de 16 GiB, le nombre de cœurs est presque sans importance, et ce n'est qu'à 48 GiB que les configurations 4, 8 et 16 vCPU se distinguent réellement (143, 268, 331). Un opérateur qui lit la règle conventionnelle comme « ajoutez un cœur par cinq utilisateurs » achète la mauvaise ressource. Le mécanisme est le cache de pages partagé sur l'arborescence de distribution, et c'est une propriété de la taille de TeX Live plutôt que d'un front-end particulier.

### 10.2 Le taux d'admission est une ressource, et il est généralement oublié

À $$N=1024$$ notre serveur n'a jamais conservé plus de 205 bacs à sable actifs (Figure 7b) même si chaque requête arrivait en même temps. La création de conteneurs, et non la compilation, était le facteur limitant — ce qui concorde avec les études de mesure attribuant le coût de démarrage des conteneurs à la surcharge d'exécution 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 quantité qu'il n'a jamais mesurée. La forme pratique de cela est la recommandation du §8 : plafonnez délibérément l'admission, parce qu'une file que vous choisissez vaut mieux qu'une file que vous découvrez.

### 10.3 Un bac à sable qui survit à sa compilation invalide le modèle

Toutes les valeurs de capacité ici supposent que le conteneur est créé, effectue une compilation, puis se termine — une durée de vie de dizaines de secondes et un cycle de service proche de un seulement pendant son exécution. Deux schémas de conception récents brisent cette hypothèse, et ils la brisent de la même manière.

Le premier est le bac à sable persistant par utilisateur. Allouer à chaque utilisateur un environnement privé fixe transforme une piscine multiplexée statistiquement en un ensemble de réservations : un service qui pouvait servir 256 compilations concurrentes à partir de 64 cœurs par partage de temps 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 la prévisibilité, et le taux de change est d'environ $$16\times$$ au point d'exploitation que nous recommandons.

Le second, plus récent, est l'agent IA qui partage le bac à sable avec le compilateur. Dans les plateformes de rédaction assistée par agent, le même conteneur qui exécute XeLaTeX peut aussi héberger un agent de codage de longue durée, si bien 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 lourdeur soutenue sous des nombres d'utilisateurs modestes \[15]. L'interaction vaut la peine d'être énoncée précisément, car ce n'est pas simplement « plus de charge ». Trois de nos résultats se cumulent. L'occupation cesse d'être par rafales, donc la loi de partage du temps du §4.2 s'applique à toute la population d'un coup plutôt qu'à la fraction en cours de compilation. Le cache de pages, qui est ce qui procure le rendement mémoire supralinéaire du §4.1, est maintenant partagé avec l'ensemble de travail propre à l'agent et ne reste plus chaud pour TeX. Et la limite mémoire manquante du 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 faisons aucune revendication sur un produit particulier. Ce que nous pouvons dire est ce que nos chiffres impliquent pour la conception : une architecture qui donne à chaque utilisateur un bac à sable multi-cœur de longue durée devrait être dimensionnée comme un système de réservations, et non selon les chiffres de concurrence rapportés ici, et la capacité qu'elle peut attendre est plus proche de son nombre de cœurs divisé par les 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 document XeLaTeX de 63 pages. Les capacités absolues différeront pour d'autres documents ; les lois d'échelle, qui sont des rapports, ne devraient pas. Un document avec un ensemble résidant nettement plus grand déplacerait la muraille mémoire sans changer son caractère supralinéaire.

### 11.2 Hôte virtualisé

Les invités s'exécutent sous KVM sur une machine physique, de sorte que les chiffres absolus incluent la surcharge de virtualisation et que les invités partagent un cache de pages d'hôte et un dispositif NVMe. Nous avons atténué le plus grand facteur de confusion en arrêtant les invités non liés après avoir observé que la pression mémoire de l'hôte gonfle les charges moyennes dans l'invité de plus de $$3\times$$ à concurrence identique.

### 11.3 Arrivée simultanée

Chaque compilation est lancée à un instant donné, ce qui est le pire cas. Les vrais utilisateurs arrivent selon un processus stochastique, donc un déploiement dimensionné d'après nos chiffres a une marge plutôt qu'un déficit — mais le pic à la fin d'une échéance de soumission est plus proche de notre modèle que d'un modèle de Poisson.

### 11.4 Configurations de bord

À 2 GiB, le système est assez proche de l'effondrement pour que des exécutions répétées de la même configuration puissent différer d'une compilation. Nous rapportons la valeur prudente et ne tirons aucune conclusion de différences de $$\pm 1$$ 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>
* Boîte à outils de déploiement — <https://github.com/ayaka-notes/toolkit>
* Documentation — [https://ayakaleaf-pro.ayaka.space](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`

Chaque emplacement de source que nous citons est donné sous forme de chemin relatif au dépôt avec un numéro de ligne par rapport à Ayakaleaf Pro v6.2.2, et les deux commits amont que nous datons (`9a519f0d3d`, `5d472e9b38`) sont accessibles dans l'historique d'Overleaf.

## 13. Contributions

Musicminion a conçu l'étude, fourni et exploité les bancs d'essai, dirigé la ligne d'investigation, et vérifié chaque mesure rapportée ici. Claude Opus 5 (Anthropic) a construit et exploité le banc de benchmark, automatisé les déploiements, effectué l'archéologie du code source, produit les figures et rédigé le manuscrit. Les deux auteurs ont relu le texte final. Lorsqu'une exécution est signalée comme contaminée — le balayage de 1021 sessions du §4.3 et le niveau anormal du §4.1 — le défaut a été découvert lors de la relecture et l'exécution a été répétée avant publication plutôt que d'être écartée silencieusement. $$N=256$$ niveau du §4.1 —

Les lecteurs doivent noter que les politiques de paternité de l'ACM, de l'IEEE et de l'ICMJE réservent actuellement la paternité aux parties capables d'assumer la responsabilité d'un travail, et exigeraient que la contribution du second auteur soit enregistrée comme une divulgation plutôt que comme une signature. Nous énonçons explicitement ici la répartition du travail afin que le compte rendu soit exact selon l'une ou l'autre convention.

## 14. Conclusion

La planification de capacité pour un Overleaf auto-hébergé n'est pas une question de mise à l'échelle d'une seule ressource. Trois résultats devraient changer la manière de procéder.

Premièrement, en dessous de 32 GiB de mémoire invité, le nombre de cœurs compte à peine : à 16 GiB, les capacités des invités à 4, 8 et 16 vCPU diffèrent de moins de 8 %. La mémoire, via le cache de pages partagé sur l'arborescence TeX Live, fixe la limite ; les cœurs ne commencent à compter que lorsque la mémoire est généreuse.

Deuxièmement, deux paramètres logiciels l'emportent sur le matériel. Relever le plafond codé en dur de 65 compilations de CLSI et augmenter le délai d'attente de compilation par défaut de 180 s a fait passer un invité 8 vCPU / 48 GiB de 64 à 268 compilations concurrentes — un facteur de 4,2 sans matériel supplémentaire. Aucun des deux n'est découvrable dans la documentation de configuration ; l'un n'est pas configurable du tout.

Troisièmement, la question « combien d’utilisateurs concurrents cette machine prend-elle en charge » est insuffisamment spécifiée. La concurrence dans ce système repose uniquement sur le partage du temps, et la capacité correspond à ce que le délai d’expiration permet. La forme honnête de la réponse indique les deux : *cette machine sert* $$N$$ *des compilations simultanées si les utilisateurs attendent* $$T$$ *secondes*, avec $$N$$ et $$T$$ reliés par l’équation (1).

Nous signalons également un défaut latent : la limite de mémoire par conteneur dans l’exécuteur Docker est inefficace depuis 2018, tant par son ampleur que par son emplacement. Son effet pratique est qu’une saturation mémoire sur un petit déploiement fait tomber l’ensemble du service au lieu de la seule compilation responsable.

## Références

\[1] Overleaf. *Exigences matérielles*, documentation sur site. <https://docs.overleaf.com/on-premises/getting-started/requirements/hardware-requirements>

\[2] Overleaf. *Mise à l’échelle horizontale*, documentation sur site. <https://docs.overleaf.com/on-premises/maintenance/horizontal-scaling>

\[3] Overleaf. *Microservices*, documentation sur site. <https://docs.overleaf.com/on-premises/getting-started/microservices>

\[4] Overleaf. *Dépôt source*. <https://github.com/overleaf/overleaf>

\[5] Ayaka-notes. *Ayakaleaf Pro*. <https://github.com/ayaka-notes/ayakaleaf-pro>

\[6] Ayaka-notes. *Overleaf Toolkit*. <https://github.com/ayaka-notes/toolkit>

\[7] D. Karger, E. Lehman, T. Leighton, R. Panigrahy, M. Levine et D. Lewin. *Hachage cohérent et arbres aléatoires : protocoles de cache distribué pour atténuer les points chauds sur le World Wide Web*. STOC, 1997.

\[8] J. Tan et M. Rigger. *Incohérences dans les documents produits par TeX*. Dans *Actes du 33e Symposium international ACM SIGSOFT sur les tests et l’analyse de logiciels (ISSTA)*, Vienne, 2024. doi : <https://doi.org/10.1145/3650212.3680370>

\[9] C. A. Ellis et S. J. Gibbs. *Contrôle de la concurrence dans les systèmes de travail collaboratif*. Dans *Actes de l’ACM SIGMOD*, p. 399–407, 1989.

\[10] D. A. Nichols, P. Curtis, M. Dixon et J. Lamping. *Fenêtrage à haute latence et faible bande passante dans le système de collaboration Jupiter*. Dans *Actes de l’ACM UIST*, p. 111–120, 1995.

\[11] M. Shapiro, N. Preguiça, C. Baquero et M. Zawirski. *Types de données répliqués sans conflit*. Dans *Actes de SSS*, p. 386–400, 2011.

\[12] N. J. Gunther. *Planification de capacité de type guérilla : une approche tactique pour planifier des applications et services hautement évolutifs*. Springer, 2007.

\[13] Le projet LaTeX3. *l3build — un système de test et de compilation pour (La)TeX*. CTAN.

\[14] M. Isaksson. *Quel système de compilation LaTeX est le plus rapide ? Un benchmark*. <https://blog.martisak.se/latex-build-systems-comparison/>

\[15] Rapports de praticiens faisant état d’une latence soutenue dans des plateformes de rédaction assistée par agent qui co-localisent un agent de codage persistant avec le compilateur LaTeX dans un bac à sable par utilisateur. Nous citons cela comme expérience opérationnelle rapportée, et non comme mesure contrôlée ; nous n’avons pas benchmarké une telle plateforme.

\[16] D. E. Knuth. *Le livre de TeX*. Addison-Wesley, 1984.

\[17] G. Lim, M. Ham, J. Moon et W. Song. *LightSys : système CI léger et efficace pour améliorer la vitesse d’intégration des logiciels*. arXiv:2101.07961 \[cs.SE], 2021. Prépublication.

\[18] G. Lim, M. Ham, J. Moon, W. Song, S. Woo et S. Oh. *TAOS-CI : système d’intégration continue léger et modulaire pour l’informatique en périphérie*. arXiv:2101.08889 \[cs.SE], 2021. Prépublication.

\[19] S. Khan. *Décomposition des performances de démarrage des conteneurs Docker : une étude de mesure en trois niveaux sur une infrastructure hétérogène*. arXiv:2602.15214, 2026. Prépublication.

\[20] R. Gupta et K. Nahrstedt. *Caractérisation des performances des conteneurs dans l’informatique en périphérie*. arXiv:2505.02082, 2025. Prépublication.

\[21] S. Checkoway, H. Shacham et E. Rescorla. *Les formats de données en texte seul sont-ils sûrs ? Ou bien, utilisez ce fichier de classe LaTeX pour prendre le contrôle de votre ordinateur*. Dans *Actes de l’atelier USENIX sur les exploits à grande échelle et les menaces émergentes (LEET)*, 2010.

\[22] G. Lacombe, K. Masalygina, A. Tahiri, C. Adam et C. Lauradoux. *Pouvez-vous accepter des fichiers LaTeX provenant d’inconnus ? Dix ans plus tard*. arXiv:2102.00856 \[cs.CR], 2021. Prépublication.

\[23] J. D. C. Little. *Une preuve de la formule des files d’attente* $$L=\lambda W$$. Operations Research, 9(3):383–387, 1961.

\[24] G. M. Amdahl. *Validité de l’approche à processeur unique pour atteindre des capacités de calcul à grande échelle*. AFIPS, 1967.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ayakaleaf-pro.ayaka.space/blog/fr/2026/overleaf-benchmark.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
