> 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/it/2026/overleaf-benchmark.md).

# Benchmark di Overleaf: un'analisi approfondita della compilazione concorrente di LaTeX in Overleaf (SaaS e self-hosted)

{% file src="/files/29832fbf3abce45155cc9096b2c02de43d3e2284" %}

## Riassunto

Le distribuzioni self-hosted di Overleaf vengono comunemente dimensionate con una sola regola empirica: un core CPU e un gigabyte di memoria ogni cinque o dieci utenti concorrenti. Mostriamo che questa regola non è solo imprecisa, ma strutturalmente sbagliata, perché assume che una sola dimensione di risorsa governi la capacità quando in realtà lo fanno due muri indipendenti, e perché due parametri software — nessuno dei quali è hardware — dominano il risultato di fattori fino a quattro.

Misuriamo una distribuzione standard di Ayakaleaf Pro v6.2.2 con compilazione in sandbox (TeX Live 2025) su 21 configurazioni CPU/memoria all'interno di guest QEMU/KVM i cui core host sono bloccati a 3.0 GHz. Il carico di lavoro è una tesi reale di 63 pagine in XeLaTeX compilata simultaneamente da diverse centinaia di account utente distinti. Troviamo che al di sotto di 32 GiB di memoria del guest il numero di core è quasi irrilevante — a 16 GiB la capacità misurata dei guest con 4, 8 e 16 vCPU differisce di meno dell'8% — e che la capacità è invece governata da un muro della memoria super-lineare dovuto alla cache delle pagine condivisa sull'albero di TeX Live.

Per chiederci se queste leggi sopravvivano a un cambiamento di scala di un ordine di grandezza, ripetiamo la scansione su un singolo server da 64 core e 995 GiB. Sostiene 1024 compilazioni a freddo simultanee con il 100% di successo — otto volte il suo numero di thread — e non raggiungiamo mai il suo limite. Il numero utile non è quel limite, ma il gomito al di sotto di esso: la latenza di coda cresce del 20–40% per ogni raddoppio fino a $$N=256$$, poi del 190% a $$N=512$$. La capacità riportata come "la massima concorrenza che non fallisce" sopravvaluterebbe quindi il punto operativo realmente utilizzabile di un fattore quattro. Su quella macchina la memoria non è mai la risorsa vincolante; il limite è la CPU insieme al tasso con cui il demone del container può ammettere nuove sandbox, che si satura intorno a 200 indipendentemente da quanti compilazioni siano richieste.

Identifichiamo inoltre due effetti a livello di implementazione invisibili alla pianificazione della capacità. Primo, CLSI impone un tetto codificato in modo rigido di 65 compilazioni simultanee che non è esposto tramite alcuna variabile d'ambiente; oltre tale limite gli utenti ricevono immediatamente HTTP 503 invece di essere messi in coda. Secondo, il limite di memoria per container nel runner Docker è inefficace dalla sua introduzione nel 2018, sia per entità sia per collocazione, quindi un evento di out-of-memory abbatte l'intero host invece di una singola compilazione. Aumentare il tetto di concorrenza e portare il timeout di compilazione predefinito da 180 s a 300 s aumenta la capacità misurata di un guest da 8 vCPU / 48 GiB da 64 a 268 compilazioni concorrenti — un fattore 4,2 a costo hardware nullo.

Infine, mostriamo che la concorrenza in questo sistema non compra altro che condivisione del tempo, e che il carico di lavoro è limitato solo dal clock. Una legge di degrado adattata $$T(N)=T\_1\max(1,N/C)^{b}$$ produce $$b=0.914$$, vicino a un rallentamento perfettamente proporzionale, e una scansione del clock sull'intero intervallo della macchina, da 1.0 a 5.5 GHz, comprime trenta misure in $$T=(k/f)\max(1,N/C)$$ con $$k=27.9 GHz·s$$ e una dispersione residua del 5,1%. Un clock 5,5× compra un'accelerazione 5,5× senza alcun rendimento decrescente, nel senso che clock e core comprano cose diverse: il clock rende più veloce la compilazione di ogni utente, i core ammettono solo più utenti.

## 1. Introduzione

Overleaf è l'editor collaborativo LaTeX dominante, e la sua distribuzione on-premises è ampiamente adottata da università e gruppi di ricerca che non possono inviare manoscritti non pubblicati a un cloud di terze parti. Dimensionare una distribuzione di questo tipo è una domanda pratica ricorrente: dato un budget hardware fisso, quante persone possono effettivamente premere "Ricompila" nello stesso momento?

La guida ufficiale è una regola lineare — all'incirca un core e un gigabyte ogni cinque-dieci utenti concorrenti — che presume che la capacità scalI senza soluzione di continuità e in modo congiunto in entrambe le risorse. Le nostre misurazioni contraddicono questo in tre modi.

### 1.1 La capacità è governata da due muri indipendenti, non da uno solo

Una configurazione fallisce o perché la memoria si esaurisce, nel qual caso lo stack di Overleaf stesso si blocca e restituisce HTTP 502, oppure perché le compilazioni superano il timeout lato server, nel qual caso CLSI riporta `timedout` mentre gigabyte di memoria restano inutilizzati. Questi due regimi hanno comportamenti di scaling completamente diversi e rimedi diversi. Aggiungere core a una configurazione limitata dalla memoria non è solo inefficiente, è talvolta controproducente: misuriamo configurazioni in cui aumentare il numero di core *riduce* la capacità, perché più core fanno avanzare le compilazioni concorrenti all'unisono, così che le loro richieste di memoria di picco coincidano invece di interlevarsi.

### 1.2 I parametri software dominano l'hardware

Il timeout di compilazione è un campo per utente in MongoDB il cui valore predefinito di 180 s limita in silenzio le configurazioni vincolate dalla CPU. Portarlo a 300 s moltiplica la capacità misurata fino a 4,2 volte a hardware invariato. Indipendentemente da ciò, CLSI rifiuta più di 65 compilazioni simultanee per una costante codificata. Qualsiasi studio di capacità — e qualsiasi distribuzione — che non tenga conto di entrambi sta misurando il software, non la macchina.

### 1.3 La concorrenza è condivisione del tempo, non parallelismo

Poiché una compilazione LaTeX è a thread singolo, servire $$N$$ utenti simultanei su $$C$$ core non fa terminare il sistema prima; fa solo aspettare ogni utente proporzionalmente più a lungo. La domanda "quanti utenti concorrenti sono supportati" è quindi mal posta finché non si fissa quanto a lungo un utente è disposto ad aspettare. Rendiamo esplicita questa dipendenza e la quantifichiamo.

### 1.4 Contributi

* Una matrice di capacità su 21 configurazioni CPU/memoria misurate in condizioni con clock bloccato e verifiche ripetute, con il vincolo vincolante identificato per ogni configurazione dalla sua firma di fallimento.
* Due modelli adattati: un modello di capacità che separa un muro della memoria super-lineare da un tetto CPU, e un modello di latenza che stabilisce un comportamento di pura condivisione del tempo.
* L'identificazione e la conferma sperimentale di due problemi di implementazione nel sistema distribuito, incluso un limite di memoria del container inattivo dal 2018.
* Una quantificazione del compromesso tra timeout di compilazione e capacità, che sosteniamo debba essere dichiarata insieme a qualsiasi cifra di concorrenza.

## 2. Contesto

### 2.1 Percorso di compilazione

Una richiesta di compilazione di Overleaf viaggia `web` $$\rightarrow$$ `clsi` $$\rightarrow$$ verso un container di compilazione. In una distribuzione con compilazioni in sandbox (`SIBLING_CONTAINERS_ENABLED=true`) CLSI non esegue `latexmk` nel processo; chiede al demone Docker dell'host, raggiungibile tramite un socket montato in bind, di avviare un nuovo container da un'immagine TeX Live con la directory del progetto montata in bind su `/compile`. Una compilazione è quindi un container di breve durata che esegue un `latexmk` processo.

Ne seguono tre conseguenze, e tutte e tre modellano le misurazioni di questo articolo. Primo, l'unità di lavoro è un processo a thread singolo: XeLaTeX non parallelizza. Secondo, l'isolamento delle risorse per compilazione è esattamente ciò che il runner Docker richiede — mostriamo in §6.2 che, di fatto, non richiede nulla. Terzo, il working set è dominato non dal documento ma dall'albero TeX Live, un corpus di sola lettura di circa 32 GiB da cui ogni compilazione concorrente legge e che quindi condivide tramite la page cache dell'host. Questa condivisione è l'origine dello scaling super-lineare della memoria che osserviamo.

### 2.2 Abilitare le compilazioni in sandbox

Overleaf Community Edition gira `latexmk` all'interno del container dell'applicazione stessa. Ayakaleaf Pro, come Overleaf Server Pro, può invece eseguire ogni compilazione in un container *fratello* — un container avviato dall'applicazione sul demone Docker dell' *host* piuttosto che annidato all'interno del container dell'applicazione. Due impostazioni del toolkit attivano questa modalità:

```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
```

Il toolkit monta in bind il socket Docker dell'host nel container dell'applicazione e traduce questi valori negli ambienti letti da CLSI: `SANDBOXED_COMPILES=true`, `SANDBOXED_COMPILES_SIBLING_CONTAINERS=true`, e `SANDBOXED_COMPILES_HOST_DIR`, l'ultimo dei quali è il percorso *host* della directory di compilazione. Quel percorso conta: poiché il demone che avvia il container di compilazione è quello dell'host, il mount bind che gli viene fornito deve essere risolvibile nello spazio dei nomi dell'host, non in quello del container dell'applicazione. Il file `config/env.sh` di Server Pro impone inoltre `TEXLIVE_IMAGE_USER=www-data` in questa modalità, così che i file scritti dal container di compilazione siano posseduti in modo coerente.

La verifica è diretta: durante una compilazione l'host mostra un container chiamato `project-{projectId}-{userId}-{hash}` in esecuzione `latexmk` dall'immagine TeX Live, che termina con codice 0. Questa è l'unità la cui molteplicità misuriamo lungo tutto l'articolo, e la cui completa assenza di limiti alle risorse riportiamo in §6.2.

**Perché questo è importante per lo studio.**

I container fratelli rendono la misurazione pulita — ogni compilazione è un'entità osservabile del sistema operativo schedulata in modo indipendente — ma significano anche che è il kernel del guest, non Overleaf, ad arbitrare CPU e memoria tra le compilazioni. Ogni legge di scaling in questo articolo è quindi una proprietà dello scheduler Linux applicato a $$N$$ processi a thread singolo, ed è per questo che è così regolare.

<figure><img src="/files/65a45ceb4b32f90a4fead2b698d1a5109382f72c" alt=""><figcaption></figcaption></figure>

**Figura 1.** Una richiesta di compilazione, tracciata attraverso i microservizi della community edition. La suddivisione nei passaggi è importante per la capacità: il testo del documento viene copiato nel corpo della richiesta, mentre le risorse binarie vengono passate per riferimento e recuperate da `clsi`. Nessuno dei due domina — il costo di compilazione di un progetto è determinato dall'albero TeX Live da 32 GiB che ogni compilazione concorrente legge tramite la cache delle pagine condivisa.

<figure><img src="/files/7090226bae0e23e7db0c5aadd12e6b2bf54dd520" alt=""><figcaption></figcaption></figure>

**Figura 2.** Tre topologie di distribuzione e dove cade, in ciascuna, il tetto di compilazione per istanza; i pannelli impilati indicano la replica. La costante di 65 compilazioni protegge *un* CLSI, quindi la flotta SaaS la moltiplica per istanze e zone (a), e lo scaling orizzontale supportato da Server Pro e Ayakaleaf Pro la moltiplica per istanze (c) — al costo di MongoDB centrale, Redis e storage compatibile S3, un bilanciatore con affinità di sessione basata su cookie (l'output della compilazione viene scritto su disco locale dell'istanza, quindi una compilazione e il successivo download del PDF devono finire sulla stessa istanza), e un git-bridge singleton. `git-bridge`. Il valore predefinito del toolkit (b), che è quello che misuriamo, ha un moltiplicatore pari a uno, quindi una costante dimensionata per un membro di una flotta diventa il tetto dell'intera installazione.

<figure><img src="/files/1e87f63c8af23597810fe1453e348deaf66afcec" alt=""><figcaption></figcaption></figure>

**Figura 3.** Selezione dello shard in `clsi-cache`. Un progetto è mappato da $$\operatorname{crc32}(\text{projectId}\text{-}i)\bmod|\text{shards}|$$, cioè lo spazio degli hash viene diviso in tanti settori uguali quanti sono gli shard. Questo è *modulo* hashing, non consistent hashing basato su anello: far crescere la flotta da tre shard a quattro ripartiziona l'intero spazio e rimappa essenzialmente ogni progetto (a, b). È proprio per questo che l'implementazione necessita di una rampa esplicita di resharing online, spostando in un intervallo di tempo una frazione che cresce linearmente di progetti da `currentShards` a `desiredShards` piuttosto che il $$K/n$$ movimento che un anello di consistent hashing darebbe. Quando scatta il circuito breaker di uno shard, il salt $$i$$ viene incrementato e lo shard rimosso dall'elenco dei candidati, così la ricerca prosegue invece di fallire (c).

### 2.3 Le due modalità di fallimento

Ogni configurazione che abbiamo misurato fallisce esattamente in uno di due modi, e la distinzione è visibile nello stato della risposta anziché essere inferita:

* **Esaurimento della memoria** — lo stack di Overleaf stesso diventa non reattivo e la richiesta restituisce **HTTP 502**. La memoria disponibile del guest al livello di fallimento è in genere inferiore a 500 MiB.
* **Timeout di compilazione** — CLSI termina la compilazione al timeout per utente e riporta lo stato `timedout`. La memoria disponibile al livello di fallimento è spesso di diversi gigabyte.

Classifichiamo ogni configurazione in base a questa firma anziché a un'euristica sui rapporti tra risorse, il che rende la domanda "quale muro abbiamo colpito" rispondibile a partire dai dati stessi.

## 3. Metodologia

### 3.1 Banco di prova e controllo del clock

Tutti i guest girano sotto QEMU/KVM su un singolo host Intel Core i9-14900K con 62 GiB di RAM e storage NVMe. Il guest è Ubuntu 24.04 con Docker 29.7 e l'Overleaf Toolkit che distribuisce Ayakaleaf Pro v6.2.2 con compilazioni in sandbox contro `texlive-full:2025.1`.

Una CPU desktop di fascia comune è un cattivo proxy per un server, a meno che il suo clock non sia controllato. KVM non offre alcun meccanismo per impostare un clock virtuale: una vCPU è un thread dell'host e gira alla frequenza a cui gira il core host. Vincoliamo quindi direttamente l'host, disabilitando il turbo e impostando `scaling_max_freq` a 3.0 GHz su ogni core, e vincoliamo le vCPU del guest ai core P fisici con `taskset`. La distinzione conta su una CPU ibrida: i core E di questo modello hanno un clock base di 2.4 GHz e *non possono* raggiungere 3.0 GHz una volta disabilitato il turbo, quindi una esecuzione che finisca su di essi misura in silenzio una macchina più lenta. A pieno carico verifichiamo esattamente 3000 MHz su tutti i sedici thread vincolati. Uno script di guardia verifica questa invarianto prima di ogni benchmark e altrimenti rifiuta di partire; durante lo studio ha intercettato un reset silenzioso del governor.

### 3.2 Un secondo banco di prova: un singolo runner grande

La matrice QEMU isola una variabile alla volta, ma si ferma a sedici thread vincolati. Per chiederci se le stesse leggi valgano ancora a un ordine di grandezza superiore, abbiamo ripetuto la scansione di concorrenza su un singolo server grande: un AMD EPYC 7773X (Milan-X, 64 core / 128 thread, 768 MiB di L3) con 995 GiB di RAM, eseguendo la stessa immagine Ayakaleaf Pro v6.2.2 contro lo stesso `texlive-full:2025.1`. A differenza dei guest QEMU questa macchina non ha il clock bloccato: è un server di classe produttiva e la misuriamo come tale.

Sono state necessarie due precauzioni operative e vale la pena dichiararle, perché senza di esse l'esperimento misura l'harness invece del server. Primo, ogni container è stato confinato in una slice `systemd` con `MemoryMax=940 GiB`, così che una scansione fuori controllo esaurisca un cgroup invece dell'host. Secondo, le compilazioni in sandbox sono create dal demone dell'host e ognuna sporca il proprio layer copy-on-write — misurato in 116 MiB per container anche se l'immagine base da 20.6 GiB è condivisa — quindi la root dei dati Docker è stata spostata su un dispositivo NVMe dedicato. Una scansione a $$N=1024$$ scrive circa 119 GiB di layer temporanei, che non entrano su un filesystem root standard.

### 3.3 Carico di lavoro

Il documento è una vera tesi di laurea magistrale di 63 pagine (template SJTU) compilata con XeLaTeX tramite `latexmk`, contenente figure TikZ, `biblatex` elaborazione della bibliografia e asset PDF incorporati — cioè un carico realistico e non sintetico. Una singola compilazione su un guest senza carico richiede 8.6–9.8 s in tutte le configurazioni, che usiamo come baseline di volo libero $$T\_1$$.

### 3.4 Generazione del carico

Creiamo 512 account utente reali e diamo a ciascuno la propria copia del progetto, così che le compilazioni concorrenti contengano esattamente come farebbero utenti indipendenti invece di condividere un lock di progetto. Le richieste sono emesse dall'host verso la porta inoltrata del guest, così la generazione del carico non consuma CPU del guest.

La concorrenza è *simultanea*, non sfalsata. Ogni sessione viene stabilita prima — login, token CSRF, selezione del compilatore — e solo allora ogni thread dorme fino a un istante di clock comune, calcolato una sola volta e condiviso, prima di emettere il proprio `POST /project/:id/compile`. La distinzione non è pedanteria. Una rampa sfalsata misura il throughput sotto una coda stabile; un'esplosione simultanea misura cosa accade quando un'aula di studenti preme lo stesso pulsante dopo lo stesso annuncio di scadenza, che è il caso che gli operatori temono davvero. I due casi differiscono di più di un fattore costante, perché il secondo riempie la coda di compilazione più rapidamente di quanto il demone riesca a svuotarla.

Prima che quell'esplosione potesse essere consegnata in modo fedele, sono stati rimossi quattro ostacoli pratici. Vale la pena registrarli, perché ciascuno degrada silenziosamente l'esperimento da misura del server a misura dell'harness.

#### 3.4.1 Due limitatori di velocità, non uno

Overleaf limita i login per indirizzo sorgente — 20 tentativi al minuto — e tutto il nostro traffico proviene da un singolo host. Assegnare a ogni utente simulato un indirizzo distinto `X-Forwarded-For` rimuove quel limite ma si scontra immediatamente con un secondo, più grossolano: un budget per sottorete di circa 200 al minuto. Distribuire gli utenti su un blocco contiguo fallisce quindi al 201° account. Deriviamo invece l'indirizzo sintetico dall'indice dell'utente, così che utenti consecutivi finiscano in sottoreti diverse `/24`s,

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

il che mantiene entrambi i limitatori scarichi per l'intera popolazione di 1024.

#### 3.4.2 L'intestazione iniettata viene scartata per impostazione predefinita

Impostare l'intestazione non è sufficiente. Express onora `X-Forwarded-For` solo per i peer di cui è stato detto di fidarsi, e il `trustedProxyIps` di Overleaf ha come valore predefinito `loopback`. Poiché il generatore di carico raggiunge l'applicazione attraverso il bridge del container invece che sull'interfaccia loopback, l'intestazione viene analizzata e poi scartata, e ogni utente simulato collassa di nuovo su un solo indirizzo. Il sintomo è un'ondata di `HTTP 429` esattamente al ventesimo login, che è facile scambiare per sovraccarico del server. La rete del gateway deve essere aggiunta esplicitamente alla catena di fiducia; nella distribuzione in cluster di §4.3 devono essere aggiunti anche i CIDR di pod e service.

#### 3.4.3 Un bilanciatore di carico sovrascriverà l'intestazione che gli era stato chiesto di preservare

Quando l'istanza si trova dietro un proxy, la convenzione `option forwardfor` *aggiunge* l'indirizzo reale del client alla catena, che è il comportamento corretto in produzione e qui esattamente sbagliato: l'indirizzo sintetico viene soppiantato da quello del generatore di carico. La direttiva deve essere qualificata come `option forwardfor if-none`, così il proxy aggiunge un valore solo quando il client non ne ha fornito alcuno.

#### 3.4.4 Il client esaurisce i descrittori di file prima che il server esaurisca la capacità

A $$N=1024$$ il generatore mantiene più di mille socket simultanei, e il limite morbido predefinito di 1024 descrittori viene raggiunto durante la configurazione della sessione anziché durante la misurazione. Il fallimento è silenzioso: tre sessioni non riescono a stabilirsi e l'esecuzione riporta 1021 invece di 1024, mentre un thread di campionamento che invoca shell per contare i container muore con `EMFILE` e tronca silenziosamente la telemetria. Il limite morbido deve essere alzato sul generatore — il limite duro sul nostro host era già 1048576 — e l'esecuzione ripetuta. Riportiamo entrambe le esecuzioni in §4.3: quella corretta completa 1024 su 1024 con una mediana entro 1.2 s da quella troncata, ed è per questo che consideriamo la prima utilizzabile ma non autorevole.

### 3.5 Protocollo di misurazione

Si sono rese necessarie diverse scelte metodologiche per la riproducibilità.

#### 3.5.1 Riscaldamento

Su un guest appena avviato la page cache è vuota e le prime compilazioni misurano l'I/O di cold-start piuttosto che la capacità a regime: la stessa configurazione da 2 vCPU / 2 GiB produce 36,5 s a freddo e 9,8 s a caldo, un fattore 3,7. Ogni configurazione esegue quindi due warm-up di una singola compilazione scartati dopo l'avvio.

#### 3.5.2 Criterio di superamento

Un livello di concorrenza supera la prova solo se *ogni* compilazione ha successo e il livello resiste a una ripetizione. Questo è più rigoroso di una soglia di tasso di successo e conta: a 4 vCPU / 16 GiB un livello di 32 è passato una volta con mediana di 80,2 s e poi è andato in timeout su tutte e 32 le compilazioni quando ripetuto, quindi riportiamo 31.

#### 3.5.3 Ricerca

I livelli vengono individuati tramite bracket esponenziale a partire da un seme previsto dal modello, seguito da una bisezione intera esatta. Poiché il criterio è tutto-o-nulla, un livello è deciso dal suo primo fallimento, quindi abbandoniamo le richieste ancora in volo una volta che una fallisce — tranne ai livelli piccoli, dove le compilazioni abbandonate bloccano un guest piccolo a tal punto che non si riprende mai.

#### 3.5.4 Isolamento tra i livelli

I container di compilazione vengono svuotati, e l'applicazione web viene interrogata fino a quando risponde di nuovo, prima che inizi il livello successivo. Senza questo, un livello che segue un crash registra un falso fallimento a zero sessioni.

#### 3.5.5 Igiene dell'host

Le VM non correlate sull'host sono state spente: con 24 GiB di memoria host impegnati altrove, la stessa configurazione del guest riportava un load average di 11,7 invece di 3,2 a concorrenza identica. La pressione di memoria dell'host si propaga nel guest e invalida la misurazione.

## 4. Risultati

### 4.1 La matrice di capacità

La Tabella 1 e la Figura 4 mostrano il limite misurato per ogni configurazione. Leggerla orizzontalmente lungo una riga è la prima sorpresa. A 4 GiB i guest da 2, 4 e 8 vCPU arrivano tutti esattamente a 9 — quadruplicare i core non cambia nulla. A 16 GiB arrivano a 54, 45 e 57: passare da 4 a 16 core compra un 6%, e il guest a 8 core è in realtà *peggiore* di quello a 4 core (§5.2). Solo a 48 GiB il numero di core separa in modo deciso le configurazioni: 143, 268 e 331.

<figure><img src="/files/370578d3470c546de3795c8f3802c4f52793945a" alt=""><figcaption></figcaption></figure>

**Figura 4.** Capacità misurata nella matrice di configurazione. (a) Ogni configurazione come barra, raggruppata per memoria e colorata per numero di core; le barre piene sono limitate dalla memoria (il guest muore per esaurimento di memoria) e le barre tratteggiate sono limitate dalla CPU (le compilazioni vanno in timeout con memoria disponibile). Leggere un gruppo da sinistra a destra mostra quanto poco compri il numero di core sotto 16 GiB; leggere tra i gruppi mostra il rendimento super-lineare della memoria. (b) Gli stessi punti rispetto al modello adattato $$N\_{\max}=\min(0.69R^{1.60},,26.4C)$$; la linea tratteggiata è il muro della memoria e le linee orizzontali puntinate sono i tetti CPU per numero di core. Una configurazione è limitata da quello dei due che incontra per primo.

Leggerla dall'alto verso il basso è la seconda: a core fissi, la capacità cresce super-linearmente con la memoria, approssimativamente come $$R^{1.6}$$, per il motivo legato alla page cache sviluppato in §5.1.

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

**Tabella 1.** Numero massimo di compilazioni simultanee che completano con successo, misurato con timeout di compilazione di 300 s e il limite di concorrenza di CLSI rimosso. **Il grassetto** indica una configurazione limitata dalla CPU (le compilazioni vanno in timeout con memoria ancora disponibile); il resto è limitato dalla memoria (lo stack muore con HTTP 502). La riga da 2 GiB riporta la correzione discussa in §5.2.

### 4.2 La concorrenza è condivisione del tempo

La Figura 5 esplora ogni livello di concorrenza su un guest fisso da 8 vCPU / 16 GiB. Due regimi sono separati da un gomito netto esattamente a una compilazione per core. Al di sotto, il tempo medio di compilazione è piatto — passa da 8,7 s a $$N=1$$ a 9,1 s a $$N=C=8$$, una variazione del 5%. Al di sopra, il tempo cresce in stretta proporzione a $$N/C$$: a $$N=16,24$$ misuriamo 18,5 s e 27,1 s, cioè un rapporto di $$1:2.13:3.12$$ contro un ideale $$1:2:3$$.

<figure><img src="/files/1451f0c5495d217e1a2859177efab582873c09f3" alt=""><figcaption></figcaption></figure>

**Figura 5.** Latenza di compilazione rispetto alla concorrenza a hardware fisso. Il gomito è a $$N=C$$; oltre quel punto il rallentamento misurato segue $$N/C$$ entro il 5–7%. Tutti e quindici i livelli hanno avuto successo completo.

<figure><img src="/files/078d09322a69d38c6fdc0ffd1aeaa76674df0631" alt=""><figcaption></figcaption></figure>

**Figura 6.** Latenza di compilazione rispetto alla concorrenza per diverse configurazioni. Ogni pannello mantiene fisso l'hardware e varia il carico offerto; la regola verticale segna $$N=C$$. Le curve sono piatte a sinistra di essa e lineari in $$N/C$$ a destra, che è la firma della condivisione del tempo e non della contesa: il lavoro non diventa più costoso, ma aspetta solo il proprio turno.

L'adattamento di $$T(N)=T\_1\max(1,N/C)^{b}$$ su tutte le misurazioni riuscite nello studio produce $$b=0.914$$ ($$R^2\_{\log}=0.904$$, $$n=81$$). Un esponente indistinguibile da uno è l'enunciato quantitativo del fatto che una compilazione è un'unità di lavoro a thread singolo, limitata dalla CPU, e che la concorrenza non aiuta né danneggia oltre il dividere i core. Il corollario pratico è scomodo per la pianificazione della capacità: una configurazione può assorbire un numero arbitrario di utenti senza *fallire* mentre fa aspettare tutti in modo proporzionalmente più lungo. A $$N=56$$ su questo guest tutte le compilazioni hanno ancora successo, ma ciascun utente aspetta 64,8 s invece di 8,7 s.

### 4.3 Scaling verticale a 1024 compilazioni concorrenti

La Tabella 2 e la Figura 7 riportano la scansione sul grande runner. Ogni livello è una compilazione *a freddo* : prima di ogni livello svuotiamo la directory di compilazione e la cache CLSI di ogni progetto partecipante tramite `DELETE /project/:id/output`, così nessun livello beneficia del lavoro svolto da quello inferiore. Il baseline di una singola compilazione su questa macchina è 28,8 s, che è il valore a freddo e non va confrontato con il baseline a regime di 8,6–9,8 s usato prima; il baseline a freddo sui guest QEMU è 28,3 s, quindi per thread le due macchine sono entro il due percento l'una dall'altra per questo carico di lavoro.

| $$N$$ |  successo | $$p\_{50}$$ | $$p\_{95}$$ | $$p\_{50}/T\_1$$ | ctrs di picco |
| ----: | --------: | ----------: | ----------: | ---------------: | ------------: |
|    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 |

**Tabella 2.** Scansione di concorrenza su un EPYC 7773X (64 core / 128 thread, 995 GiB). Tutti i livelli a freddo; baseline 28,8 s. I container al picco sono il numero massimo di sandbox vive simultaneamente.

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

**Figura 7.** Scaling verticale su un grande runner. (a) Latenza rispetto alla concorrenza offerta; l'area ombreggiata indica il regime oltre il gomito. (b) Il numero di sandbox effettivamente vive non segue mai il numero richiesto — si satura intorno a 200 — mentre il cgroup di compilazione non usa mai più di un quinto del suo limite.

#### 4.3.1 La macchina non fallisce mai

Ogni livello si completa al 100%, compreso $$N=1024$$ — otto volte il numero di thread. Non abbiamo trovato il tetto di capacità di questa macchina; siamo rimasti senza pazienza prima che finisse la testa libera. Questa è la prima configurazione nello studio in cui il vincolo vincolante non è la memoria: a $$N=1024$$ il cgroup di compilazione raggiunge un picco di 184 GiB, un quinto del suo limite di 940 GiB, mentre la CPU è al 100% di utilizzo con un load average di 166.

#### 4.3.2 Il degrado è sub-lineare perché l'ammissione è limitata dal tasso

La condivisione ingenua del tempo prevede che $$8\times$$ i thread costino $$8\times$$ la latenza. Il costo misurato è $$9.7\times$$ rispetto a una singola compilazione, ma solo $$3.8\times$$ rispetto a $$N=128$$ — per un aumento di carico di otto volte. Il motivo è visibile nella Figura 7(b) e nell'ultima colonna della Tabella 2: sebbene 1024 richieste siano emesse simultaneamente, il numero di sandbox effettivamente vive non supera mai 205. Il demone non riesce a creare container alla stessa velocità con cui i client li richiedono, quindi le richieste si mettono in coda all'ammissione invece di contendere all'interno della CPU. È la coda a salvare qui la coda lunga, e lo fa per accidente.

#### 4.3.3 Il gomito è a 512, non al punto di fallimento

Tra $$N=256$$ e $$N=512$$ il $$p\_{95}$$ la latenza cresce di $$2.9\times$$ per un raddoppio del carico; ogni raddoppio precedente costava tra $$1.2\times$$ e $$1.4\times$$. Capacità espressa come "il più grande $$N$$ che non fallisce" riporterebbe 1024 e sarebbe inutile per un operatore: a quel punto l'attesa di coda è di quasi otto minuti.

### 4.4 Il tempo di compilazione è inversamente proporzionale al clock

Poiché il carico di lavoro è limitato dalla CPU, il suo costo dovrebbe scalare come $$1/f$$. Lo testiamo direttamente spazzando il clock dell'host sull'intero intervallo della macchina, 1.0–5.5 GHz in dieci passi, su un guest altrimenti invariato (Figura 8). Il tempo di una singola compilazione passa da 26,5 s a 4,8 s: un clock 5,5× compra un'accelerazione 5,5× senza alcun rendimento decrescente in tutto l'intervallo. Il prodotto $$T!\cdot!f$$ è costante entro il 2% su tutti e dieci i clock.

Normalizzando per la quota di core si comprimono tutte e trenta le misure — tre livelli di concorrenza a dieci clock — in una sola costante:

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

con una dispersione residua del 5,1% su un intervallo in cui il clock stesso varia di 5,5×. L'assenza di qualsiasi curvatura è essa stessa il risultato: se il carico di lavoro fosse stato limitato dalla banda della memoria o dall'I/O, $$T$$ si appiattirebbe a clock elevato quando la CPU superasse l'altra risorsa.

<figure><img src="/files/21d21fe607b1f925ca0f1f66ca03fab4d4ca0ed2" alt=""><figcaption></figcaption></figure>

**Figura 8.** Scansione del clock. (a) $$T=k/f$$ con l'iperbole adattata. (b) Dopo la divisione per $$\max(1,N/C)$$ tutti i punti si comprimono in un'unica costante, confermando l'Equazione (1).

L'Equazione (1) ha una conseguenza diretta sugli acquisti, facile da enunciare e facile da interpretare male: *il clock migliora l'esperienza di ogni singolo utente, il numero di core ammette solo più utenti*. Una macchina con un clock più alto del 20% compila per tutti il 20% più velocemente, senza alcun rendimento decrescente; il doppio dei core non rende più veloce la compilazione di nessuno.

## 5. Analisi

### 5.1 Due muri, adattati separatamente

Ogni configurazione è classificata dalla sua firma di fallimento (§2.3), e il muro della memoria e il tetto CPU vengono quindi adattati solo sulle configurazioni che li hanno effettivamente colpiti:

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

con $$R$$ in gibibyte e $$C$$ in vCPU.

L'esponente del memory wall è costantemente superlineare, $$p>1$$: il costo marginale di memoria di una compilazione concorrente aggiuntiva *diminuisce* al crescere della memoria totale, da circa 312 MiB per compilazione su un guest da 3 GiB a circa 194 MiB su uno da 32 GiB. Il meccanismo è la cache di pagina condivisa sull'albero di TeX Live descritta in §2.1: le compilazioni concorrenti leggono file di font e macro sovrapposti, quindi una cache più grande viene ammortizzata su un numero maggiore di esse. Ecco perché la regola ingenua “un gigabyte ogni cinque utenti” sottostima le macchine grandi e sovrastima quelle piccole.

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

**Figura 9.** Gli stessi dati come due superfici sul $$(C,R)$$ piano. (a) Capacità: la superficie stimata è una dorsale, non un piano — sale ripidamente con la memoria ed è quasi piatta lungo l'asse dei core finché la memoria non smette di vincolare, ed è per questo che la riga da 48 GiB è l'unica in cui il numero di core separa le configurazioni. (b) Latenza rispetto alla concorrenza per ogni configurazione, con la curva stimata $$T=12.6,(N/C)^{0.91}$$ mostrata tratteggiata e il timeout di 180 s disegnato come un piano. Una configurazione fallisce nel punto in cui la sua curva continua perfora quel piano, il che rende visibile quanto direttamente l'impostazione del timeout determini la capacità riportata.

### 5.2 Dove più core peggiorano la situazione

L'equazione (2) è il minimo di due termini e quindi monotona in $$C$$, ma le misurazioni non lo sono. Osserviamo due inversioni in cui l'aggiunta di core *ha ridotto* la capacità: a 16 GiB (54 contro 45) e a 32 GiB (145 contro 135). Entrambe avvengono nel regime limitato dalla memoria, e il meccanismo è lo stesso in ciascun caso: con più core, le compilazioni concorrenti avanzano all'unisono e raggiungono la loro dimensione residente di picco nello stesso momento, mentre con meno core lo scheduler le interlaccia e i picchi sono sfalsati. Su un guest il cui margine di memoria è già esiguo, lo sfalsamento è ciò che lo mantiene in vita. Un modello di capacità costruito sull'uso medio delle risorse non può esprimerlo; è una proprietà della *coincidenza* dei picchi.

Una terza apparente inversione, a 2 GiB, ora la escludiamo. Il log di ricerca registra capacità 2 a 2 vCPU ma 1 a 4 e 8 vCPU, il che sembra lo stesso effetto. Riesaminando le scansioni grezze emerge qualcosa di più semplice: a 2 GiB il livello $$N=2$$ è riuscito al primo tentativo con tutti e tre i conteggi di core e poi ha fallito la corsa di conferma in due su tre. Il livello non è una capacità ma un lancio di moneta, e la voce a 2 vCPU è il risultato capitato per caso. Riportiamo quindi il valore riproducibile, 1, per tutti e tre i conteggi di core e non traiamo alcuna conclusione dalla differenza. Registriamo qui la correzione invece di ristampare silenziosamente la tabella, perché la lettura scartata è il tipo che avrebbe sostenuto un'affermazione interessante.

## 6. Risultati di implementazione

### 6.1 Un limite di concorrenza codificato in modo rigido

Su guest sufficientemente grandi, la capacità si è fermata esattamente a 65 compilazioni simultanee, indipendentemente dalla concorrenza richiesta: a $$N=66,80,96,128$$ abbiamo misurato $$65$$ successi e $$1,15,31,63$$ immediati `non disponibili` risposte, con il numero di container bloccato a 65, diversi gigabyte di memoria inutilizzati e il tempo mediano di compilazione stabile a 77 s — ben al di sotto di qualsiasi timeout.

La causa è una costante in 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   // <= ammette 64+1
throw new Errors.TooManyCompileRequestsError(...)
```

Il confronto non è stretto, quindi il limite effettivo è $$64+1=65$$, corrispondendo esattamente alla misurazione. Le richieste in eccesso ricevono **HTTP 503** — vengono *rifiutate*, non messe in coda, quindi dal lato dell'utente il pulsante di compilazione semplicemente fallisce. A differenza di ogni altro parametro regolabile nello stesso file, questo non legge alcuna variabile d'ambiente; è stato introdotto a monte nell'agosto 2024 e può essere cambiato solo modificando l'immagine. Con il limite aumentato, lo stesso guest da 16 vCPU / 32 GiB che riportava `success=65, unavailable=15` a $$N=80$$ ha riportato invece `success=80`.

### 6.2 Un limite di memoria del contenitore non operativo

L'ispezione di un contenitore di compilazione in esecuzione non mostra alcun isolamento delle risorse:

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

L'assenza di qualsiasi quota CPU è intenzionale e spiega perché l'esponente di time-sharing del §4.2 sia così pulito: nulla distorce la competizione tra compilazioni. L'assenza di un *memoria* limite, invece, non è intenzionale. Il runner Docker ne richiede uno:

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

È sbagliato in due modi. $$1024^4=1 tebiB$$ dove il commento intende $$1024^3$$; e il campo è collocato al livello superiore delle opzioni di creazione invece che dentro `HostConfig`, dove l'API Docker se lo aspetta, quindi viene scartato — come conferma il `Memory=0` conferma. Entrambi gli errori sono presenti nel commit che ha introdotto il file (`9a519f0d3d`, marzo 2018) e sono sopravvissuti alla conversione da CoffeeScript, a una riformattazione dell'intero repository e a una migrazione da CJS a ESM, nessuna delle quali ha rivisto la semantica. In particolare `MAX_OUTPUT = 1024 * 1024 // 1MB` nello stesso commit è corretto, indicando una svista piuttosto che un malinteso.

La conseguenza è visibile nelle nostre misurazioni a bassa memoria. Poiché le compilazioni non hanno limiti, l'esaurimento della memoria non si manifesta come Docker che termina un singolo container colpevole; manda giù l'intero guest. Sulla configurazione da 2 vCPU / 2 GiB abbiamo osservato la sessione SSH di monitoraggio bloccata per 300 s, un load average di 68 su due core e il guest che alla fine si è riavviato da solo. Un limite per contenitore funzionante degraderebbe molto più elegantemente: la compilazione troppo grande fallirebbe e il servizio sopravviverebbe.

L'unico limite che entra effettivamente in vigore è `RLIMIT_CPU`, impostato a $$\text{timeout}+5$$ secondi. Vincola il *tempo di CPU* tempo, non il tempo di wall-clock, e una singola compilazione consuma solo circa 9 s di CPU, quindi non vincola mai a nessun livello di concorrenza; protegge da input patologici come una macro fuori controllo. È tuttavia un oracolo utile: osservare `Soft:305` conferma che un'impostazione di timeout di 300 s si è effettivamente propagata al contenitore.

### 6.3 Il timeout di compilazione è il parametro regolabile dominante

Il campo per utente `features.compileTimeout` ha come valore predefinito 180 s. Per qualsiasi configurazione limitata dalla CPU questo non è un margine di sicurezza ma un'impostazione di capacità, perché una macchina che sta ancora computando correttamente viene dichiarata fallita. Portarlo a 300 s — un singolo aggiornamento MongoDB — cambia la capacità misurata fino a un fattore di 4,2 (Tabella 3). Il limite massimo è 600 s, imposto da `RequestParser.MAX_TIMEOUT`, oltre il quale il valore viene troncato silenziosamente.

| Configurazione   | 180 s | 300 s |       Rapporto | Vincolo limitante                 |
| ---------------- | ----: | ----: | -------------: | --------------------------------- |
| 8 vCPU / 48 GiB  |    64 |   268 | $$4.19\times$$ | CPU, 28 GiB liberi                |
| 4 vCPU / 32 GiB  |    47 |   145 | $$3.09\times$$ | CPU, 30 GiB liberi                |
| 4 vCPU / 48 GiB  |    63 |   143 | $$2.27\times$$ | tempo di CPU                      |
| 4 vCPU / 16 GiB  |    31 |    54 | $$1.74\times$$ | tempo di CPU                      |
| 2 vCPU / 8 GiB   |    15 |    21 | $$1.40\times$$ | tempo di CPU                      |
| 8 vCPU / 32 GiB  |   127 |   135 | $$1.06\times$$ | colpisce per primo il memory wall |
| 8 vCPU / 16 GiB  |    56 |    45 | $$0.80\times$$ | memoria                           |
| 16 vCPU / 32 GiB |   159 |   141 | $$0.89\times$$ | memoria                           |

**Tabella 3.** Effetto del timeout di compilazione sulla capacità misurata.

Le ultime due righe sono la parte controintuitiva del risultato e il motivo per cui abbiamo rimisurato ogni configurazione con un unico timeout. Per *memoria*configurazioni vincolate dalla CPU, un timeout più lungo *riduce* aumenta la capacità, perché ogni compilazione mantiene il proprio resident set per più tempo e più di esse si sovrappongono. Una cifra di capacità è quindi priva di significato se non si indica il timeout sotto cui è stata misurata, e le due cose non possono essere mescolate in una sola tabella.

## 7. Lavori correlati

### 7.1 Indicazioni del fornitore

La documentazione hardware di Overleaf stessa afferma i fatti qualitativi che quantifichiamo qui: che LaTeX è single-threaded, che quindi le prestazioni di un singolo core governano il tempo di compilazione, e che “più core aiutano solo se stai cercando di compilare più documenti di quanti core CPU liberi tu abbia” \[1]. Fornisce poi la regola di dimensionamento lineare — una base di 2 core/3 GiB più un core e un gigabyte ogni cinque-dieci utenti concorrenti — che ha motivato questo studio. Il nostro contributo è trasformare queste affermazioni in leggi misurate (Equazioni (1) e (2)) e mostrare dove la regola lineare si rompe: non ha un termine per la cache di pagina condivisa che rende superlineare il memory wall, e nessun termine per i due parametri software che dominano il risultato.

### 7.2 Studi sulla capacità di build e CI

La misurazione dei sistemi di build sotto concorrenza è ben consolidata al di fuori dell'ambiente LaTeX. LightSys riferisce che i sistemi CI convenzionali che compilano all'interno di container Docker degradano nell'I/O all'aumentare del tasso di arrivo delle pull request, con un collo di bottiglia che appare intorno a undici richieste concorrenti \[17]; TAOS-CI osserva che la compilazione domina il tempo di wall-clock della CI, rappresentando il 60–67% della durata totale della pipeline su progetti grandi \[18]. Il nostro sistema differisce in un aspetto che si rivela decisivo: una compilazione LaTeX è interattiva. Un job CI che impiega il doppio del tempo è un fastidio; una compilazione che impiega il doppio del tempo è osservata direttamente da un utente che aspetta un riquadro di anteprima, ed è per questo che trattiamo il timeout non come una soglia di errore ma come un parametro di capacità.

### 7.3 Overhead dei container

Lavori recenti scompongono la latenza di avvio dei container Docker tra i diversi livelli di archiviazione \[19] e caratterizzano le prestazioni dei container ai margini \[20]. Nel nostro contesto l'avvio del contenitore per ogni compilazione viene ammortizzato: è una piccola costante rispetto a una compilazione di 9 s, e il tempo di free-flight $$T\_1$$ che stimiamo assorbe quel costo. La proprietà del contenitore che conta davvero è l' *assenza* di limiti di risorse (§6.2), che trasforma un superamento della memoria per compilazione in un guasto dell'intero host.

### 7.4 LaTeX come input non attendibile

La compilazione in sandbox esiste perché TeX è un linguaggio di programmazione e i documenti sono input non attendibili \[21, 22]. Questa scelta progettuale è ciò che rende possibile questo studio — ogni compilazione è un contenitore isolato con comportamento delle risorse osservabile — e anche ciò che rende conseguente il limite di memoria mancante, poiché l'isolamento è assunto dagli operatori che lo distribuiscono.

### 7.5 Il compilatore come oggetto di studio

TeX stesso è ben documentato come linguaggio \[16], ma il suo comportamento come *obiettivo di build* ha attirato attenzione solo di recente. Tan e Rigger \[8] compilano un ampio corpus di sorgenti arXiv su diversi motori e versioni di distribuzione e scoprono che la scelta del motore non è sostituibile: solo una frazione di punto percentuale dei documenti produce output identico byte per byte con XeTeX e pdfTeX. Quel risultato incide direttamente sulla nostra metodologia. La capacità è una proprietà di un documento *e* un motore, quindi un benchmark che non fissa entrambi non è riproducibile; fissiamo quindi un documento, un motore e una distribuzione (`texlive-full:2025.1`) per tutta la durata, e riportiamo il motore in ogni didascalia di figura. Inoltre limita la generalità dei nostri numeri in un modo che vale la pena dichiarare chiaramente: caratterizzano XeLaTeX su questo documento, non TeX in astratto.

Il lavoro sui build LaTeX *sistemi* è in gran parte guidato dai praticanti. Il progetto LaTeX3 `l3build` \[13] standardizza i test di regressione e il packaging, e benchmark indipendenti confrontano strumenti wrapper — un'indagine su 26 sistemi di build trova che un preambolo precompilato vale circa il 20% in più rispetto a un'esecuzione normale e il 40% in più rispetto a `latexmk` \[14]. Questi ottimizzano la *singola* compilazione. Sono ortogonali a, e si compongono con, ciò che misuriamo: una cache del preambolo accorcia $$T\_1$$, e ogni cifra di capacità in questo articolo scala con $$T\_1$$.

### 7.6 Controllo della concorrenza nell'editor, non nel compilatore

La parte collaborativa di Overleaf si basa su una linea di ricerca consolidata. La trasformazione operazionale ha origine con Ellis e Gibbs \[9] ed è stata resa pratica per client ad alta latenza dal sistema Jupiter \[10], il cui design è riconoscibile in `document-updater`: un server che ordina le operazioni e un buffer per documento su cui i client si sincronizzano. I tipi di dati replicati senza conflitti \[11] risolvono lo stesso problema senza un sequencer centrale. Questa distinzione è ciò che rende funzionante la topologia del §4.3: poiché il buffer degli aggiornamenti in sospeso vive in Redis condiviso invece che nella memoria di un'istanza, una compilazione instradata a qualunque replica osserva gli ultimi tasti premuti, e l'affinità di compilazione può essere scelta per la località della cache anziché per la correttezza.

### 7.7 Modelli di capacità

La legge di Amdahl \[24] limita l'accelerazione dovuta al parallelismo e la legge di Little \[23] mette in relazione occupazione, tasso di arrivo e tempo di servizio; entrambe sono usate sopra. La legge universale della scalabilità di Gunther \[12] estende la prima con un termine retrogrado per il ritardo di coerenza, prevedendo che il throughput raggiunga un picco e poi declini. Notiamo che il nostro sistema *non* mostra quel regime retrogrado fino a $$N=1024$$: il throughput si satura e la latenza cresce, ma nulla crolla. La ragione è strutturale piuttosto che fortunata — le compilazioni non condividono stato da mantenere coerente, quindi il termine aggiunto dalla legge è prossimo a zero, e il plateau di ammissione del §4.3 limita la contesa prima che possa incidere.

## 8. Raccomandazioni per gli operatori

{% stepper %}
{% step %}

## Correggi i due parametri software prima di comprare hardware

Entrambi sono gratuiti e valgono più di qualsiasi singolo upgrade hardware che abbiamo misurato. Porta `features.compileTimeout` a un valore che i tuoi utenti tollereranno davvero — il massimo accettato da CLSI è 600 s — e, se prevedi di superare 65 compilazioni simultanee, o alza `compileConcurrencyLimit` in un'immagine derivata oppure scala orizzontalmente. Non fare nessuna delle due cose significa pagare per core che il software si rifiuta di usare.
{% endstep %}

{% step %}

## Dimensiona una macchina in base al suo ginocchio, non al suo limite superiore

La scansione sul grande runner (§4.3) separa due numeri che vengono abitualmente confusi. Il *limite superiore* — la massima concorrenza che restituisce ancora ogni PDF — è almeno 1024 su un server a 64 core, e non l'abbiamo mai raggiunta. Il *ginocchio* — il punto oltre il quale la latenza di coda smette di crescere dolcemente e inizia a raddoppiare — è a 512, e l'ultimo punto operativo confortevole al di sotto è 256. Tra $$N=256$$ e $$N=512$$ il $$p\_{95}$$ l'attesa passa da due minuti a quasi sei; tra 512 e 1024 arriva a otto. Un operatore che dimensiona sul limite superiore spedisce un sistema che tecnicamente funziona e che nessuno vuole usare.

Per questa macchina e questo documento il punto operativo raccomandato è dunque **256 compilazioni concorrenti**, che corrispondono a $$4\times$$ il numero fisico di core e $$2\times$$ il numero di thread, e che mantengono $$p\_{95}$$ intorno a 120 s. Suggeriamo di impostare `compileConcurrencyLimit` a quel valore piuttosto che lasciarlo alto: ammettere 1024 compilazioni tutte insieme fa attendere tutti otto minuti, mentre ammettere 256 e mettere in coda il resto serve la maggior parte degli utenti in due. La coda penalizza gli arrivi tardivi; la contesa penalizza tutti.
{% endstep %}

{% step %}

## Considerali numeri di caso peggiore

Ogni livello in Tabella 2 è una compilazione a freddo avviata simultaneamente. Nessuna delle due condizioni vale in produzione: una compilazione a caldo dello stesso documento richiede 8,6 s contro 28,3 s a freddo, un fattore di $$3.3$$, e gli utenti reali non premono il pulsante nello stesso secondo. Una popolazione a regime che ricompila ogni due minuti con un tipico tasso di hit della cache sosterrà quindi molti più autori di quanto suggerisca il solo numero di concorrenza — nell'ordine di un migliaio o più di autori attivi al punto operativo 256. La cifra di concorrenza è un limite del burst istantaneo, non un conteggio di posti.
{% endstep %}

{% step %}

## Decidi prima il budget di latenza, poi ricava la dimensione

L'equazione (1) si inverte direttamente. Per un'attesa obiettivo $$T$$ a clock $$f$$ su $$C$$ core, la concorrenza che rientra è $$N \le C,fT/k$$ con $$k\approx28 GHz·s$$ per questo documento. Un budget di 60 s su 8 core a 3 GHz dà $$N\le51$$; un budget di 120 s lo raddoppia. Pubblicare il budget insieme alla capacità è l'unico modo onesto di dichiarare entrambi.
{% endstep %}

{% step %}

## Compra prima memoria, poi core, e verifica contro quale wall ti stai scontrando

Sotto 32 GiB abbiamo misurato quasi nessun beneficio da core aggiuntivi. La diagnosi è semplice: se i guasti compaiono come HTTP 502 con il guest a corto di memoria, aggiungi memoria; se compaiono come `timedout` con memoria da vendere, aggiungi core o aumenta il timeout. Gli operatori possono dedurlo dalla stessa firma di errore che abbiamo usato per classificare le configurazioni.
{% endstep %}

{% step %}

## Preferisci il clock per l'esperienza, i core per la popolazione

Poiché $$T\propto 1/f$$ vale senza curvatura (2% tra 1,0 e 5,5 GHz), un clock più veloce rende ogni compilazione più rapida per ogni utente. Più core non rendono più rapida alcuna singola compilazione; ammettono solo più compilazioni concorrenti. Le distribuzioni la cui lamentela è “le compilazioni sono lente” dovrebbero comprare clock; quelle la cui lamentela è “le compilazioni falliscono alla scadenza” dovrebbero comprare memoria e core.
{% endstep %}

{% step %}

## Scala orizzontalmente piuttosto che verticalmente oltre il limite

Oltre 65 compilazioni concorrenti, il percorso supportato è lo scale-out orizzontale (Figure 2c e 10, approfondite in §9): più istanze dell'applicazione dietro un bilanciatore di carico con affinità di sessione basata su cookie, che condividono MongoDB centrale, Redis e storage compatibile con S3, con `git-bridge` lasciato come singleton. Questo moltiplica il limite per istanza per il numero di istanze, che è esattamente il modo in cui la distribuzione SaaS raggiunge la propria capacità.
{% endstep %}

{% step %}

## Non affidarti all'isolamento per compilazione

Finché il limite di memoria del runner Docker non viene corretto (§6.2), un singolo documento patologico può esaurire l'host anziché essere ucciso da solo. Gli operatori che hanno bisogno di quella garanzia dovrebbero imporla da sé invece di aspettarla. Il meccanismo che abbiamo usato sul grande host è una slice systemd con un limite rigido, verso cui viene poi puntato il demone Docker in modo che ogni contenitore che crea venga contabilizzato al suo interno:

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

Un dettaglio in questo fa perdere un pomeriggio se lo si manca. Una slice chiamata `docker-capped.slice` non si affianca a `docker.slice`; si trova *all'interno di* essa, perché il trattino è il separatore della gerarchia e non parte del nome. Un limite superiore che sembra non avere effetto è di solito stato applicato un livello lontano da dove i contenitori vivono davvero. Verifica leggendo il picco da `memory.max_usage_in_bytes` dopo un'esecuzione invece di fidarti del file di configurazione — sul nostro host il cgroup di compilazione non ha mai superato un quinto del suo limite superiore neanche con 1024 compilazioni simultanee, il che è di per sé la prova che il vincolo limitante era il demone e non la memoria.
{% endstep %}
{% endstepper %}

<figure><img src="/files/54b4cba45719d875a653545f235358906c4e24f2" alt=""><figcaption></figcaption></figure>

**Figura 10.** Topologia di riferimento per una distribuzione scalata orizzontalmente, tratta dalla configurazione che abbiamo verificato. Le repliche dell'applicazione sono intercambiabili e non mantengono nulla di durevole, quindi possono essere aggiunte e rimosse liberamente. Tre componenti non lo sono: Redis, il cui buffer dei documenti è ciò che consente a una compilazione instradata a qualunque replica di vedere i tasti più recenti; l'archivio oggetti, che diventa obbligatorio anziché opzionale oltre una replica; e `git-bridge`, che mantiene i repository su disco locale senza alcun percorso di replica e deve funzionare come singleton accanto a una replica designata.

## 9. Una distribuzione di riferimento su più macchine

Tutto quanto sopra misura una sola macchina. Questa sezione descrive la forma distribuita con abbastanza dettaglio da poterla costruire, e — poiché la domanda che un operatore si pone davvero non è *come* ma *se* — indica prima il punto in cui vale la pena di farlo.

### 9.1 Quando la forma distribuita è giustificata

Una sola macchina è più economica da gestire sotto ogni aspetto che conta: un solo dominio di guasto, nessuno stato condiviso da mantenere coerente, nessun routing da sbagliare. I nostri dati fissano tre soglie su quando abbandonarla.

#### 9.1.1 Al di sotto di 65 compilazioni concorrenti, non

Il limite per istanza è una costante software, non hardware (§6.1). Finché il carico offerto non vi si avvicina, una seconda macchina aggiunge modalità di guasto e non porta nulla. L'host a 64 core ha servito 256 compilazioni concorrenti con successo completo solo dopo che `compileConcurrencyLimit` è stato alzato; un operatore che non ha ancora modificato quel singolo valore non è limitato dall'hardware e non dovrebbe andare a comprare hardware.

#### 9.1.2 Tra 65 e circa 500, scala prima in verticale

Lo scale-up verticale è rimasto lineare lungo tutto il nostro intervallo e non è mai entrato in un regime retrogrado. Un grande host ha raggiunto 1024 compilazioni a freddo simultanee con successo al 100% (§4.3); il ginocchio della latenza è apparso a 512, non prima. All'interno di quella fascia una macchina più grande è di gran lunga più semplice di diverse più piccole e, secondo §4.4, una più veloce migliora l'esperienza di ogni utente invece di ammetterne semplicemente di più.

#### 9.1.3 Vai distribuito per disponibilità, non per throughput

La ragione onesta per eseguire più di una replica dell'applicazione al di sotto del limite superiore è che una macchina è un alimentatore, un kernel, una finestra di aggiornamento. È una ragione legittima ed è quella che daremmo; semplicemente non è un argomento di capacità, e confondere le due cose porta gli operatori a comprare repliche quando invece avevano bisogno di memoria.

### 9.2 Livelli e loro dimensionamento

La Figura 10 mostra la topologia. Ha quattro livelli, e scalano su quantità diverse — che è proprio il motivo per cui separarli.

#### 9.2.1 Edge

Un bilanciatore di carico, o due per disponibilità. Termina TLS e non fa nulla di costoso; scala con il numero di connessioni, non con il numero di compilazioni, e un'istanza piccola è sufficiente per i carichi studiati qui. Ciò che conta è la sua configurazione, non la sua dimensione (§9.3).

#### 9.2.2 Repliche dell'applicazione

Queste gestiscono il carico di compilazione e sono l'unico livello che scala con la concorrenza. Dimensiona ciascuna secondo le regole del §8 — memoria prima dei core, poi il clock — e poi imposta il numero di repliche per coprire la concorrenza di picco divisa per il limite per replica. Le repliche non conservano nulla di durevole: il loro disco locale contiene i file temporanei di compilazione e una cache di output, entrambi ricostruibili. Questo è ciò che le rende sicure da aggiungere e rimuovere liberamente, ed è utile verificarlo anziché darlo per scontato, perché un singolo `filestore` configurato male converte silenziosamente il livello in uno con stato.

#### 9.2.3 Stato

Redis, MongoDB e un archivio oggetti compatibile con S3, su host separati. Redis è quello portante e il meno ovvio: contiene lo store delle sessioni e il buffer del documento live, che è ciò che permette a una compilazione instradata a qualunque replica di osservare i tasti premuti su un'altra replica. Un operatore che tratta Redis come una cache e lo dimensiona per l'evizione produrrà compilazioni di documenti obsoleti che sono estremamente difficili da diagnosticare, perché nulla fallisce — il risultato è semplicemente sbagliato. MongoDB scala con il numero di progetti piuttosto che con il tasso di compilazione. L'archivio oggetti è opzionale con una replica e obbligatorio oltre quella.

#### 9.2.4 Il singleton

`git-bridge` mantiene i repository su disco locale, mantiene un indice locale e non ha alcun percorso di replica. Deve funzionare come esattamente una istanza, fissata accanto a una replica designata, ed è il componente che rende la distribuzione non del tutto senza stato. Pianifica di conseguenza il suo host: il suo disco è quello che deve essere sottoposto a backup.

| **Livello**            | **Conteggio**        | **Scala con**                     |
| ---------------------- | -------------------- | --------------------------------- |
| Bilanciatore di carico | 1–2                  | connessioni concorrenti           |
| Applicazione           | $$\lceil N/C\rceil$$ | picco di compilazioni concorrenti |
| Redis                  | 1 (+replica)         | sessioni di modifica attive       |
| MongoDB                | 1 (+replica)         | progetti archiviati               |
| Archivio oggetti       | 1 cluster            | byte totali dei progetti          |
| `git-bridge`           | 1 esattamente        | repository su disco               |

**Tabella 4.** Livelli di riferimento. Solo il livello applicativo scala con la concorrenza; il suo dimensionamento è l'oggetto del §8.

### 9.3 Il routing è la parte facile da sbagliare

Tre classi di richiesta devono raggiungere tre posti diversi, e la configurazione predefinita a una sola regola ne soddisfa al massimo due.

Il traffico di compilazione sotto `/project/` dovrebbe essere distribuito con hashing consistente sull'identificatore del progetto, così che la cache di compilazione di un progetto rimanga con una sola replica. Usiamo la combinazione di HAProxy `balance hash path,field(3,/)` con `hash-type consistent` e `hash-balance-factor 150`. La scelta conta nello scale-out: con l'affinità basata su cookie, le sessioni esistenti restano legate alla loro replica originale a tempo indeterminato e una replica aggiunta di recente riceve solo nuovi utenti, quindi la macchina che un operatore ha appena pagato non assorbe alcun carico che giustifichi l'acquisto. L'hashing consistente ha ridistribuito il 35% dei progetti nello scale-out nella nostra configurazione, contro lo 0% dei cookie.

Il traffico di sessione è diverso. Quando l'upgrade WebSocket fallisce e `socket.io` ricade sul polling XHR, i polling successivi di una sessione devono raggiungere una replica, e nel percorso non c'è alcun identificatore di progetto da sottoporre a hash. Questo traffico necessita di un backend separato con affinità basata su cookie. Abbiamo progettato questa separazione ma non l'abbiamo distribuita; la segnaliamo come una lacuna piuttosto che rivendicarla.

Infine, `/git/` deve raggiungere la replica accanto alla quale `git-bridge` gira. Viene instradato a quella replica invece che a `git-bridge` direttamente perché il bridge autentica i propri callback contro gli endpoint OAuth dell'applicazione e risolve tramite esso gli URL blob; aggirare la replica rompe l'autenticazione invece di migliorare qualcosa.

### 9.4 Lo scale-in ha bisogno di un buffer di drenaggio

Rimuovere una replica non è simmetrico all'aggiungerne una: una compilazione in corso viene persa e l'utente vede un fallimento che non ha causato. La sequenza praticabile è fermare prima il nuovo traffico, attendere e solo poi terminare. Abbiamo implementato questo come un hook pre-stop che trattiene il pod per un intervallo configurabile mentre il bilanciatore marca il backend in drenaggio — abbastanza breve da testare in minuti, e in produzione abbastanza lungo da coprire la fine naturale di una sessione, ore anziché secondi. L'intervallo è la manopola di regolazione che decide se l'elasticità è invisibile o esasperante.

Un ulteriore vincolo che abbiamo trovato per misurazione e non per progettazione: l'autoscaling basato sulla CPU non funziona per questo carico di lavoro. L'utilizzo del pod dell'applicazione era pari a 22 m core contro un totale di nodo di 3997 m core, perché il lavoro di compilazione avviene in container fratelli che il pod non contabilizza. Qualsiasi segnale usato per scalare questo livello deve contare i container di compilazione in esecuzione, non la CPU del pod.

## 10. Implicazioni oltre Overleaf

Nulla in §4.2 o §4.3 è specifico del codice di Overleaf. Le leggi misurate derivano da tre proprietà condivise da qualsiasi servizio LaTeX ospitato: l'unità di lavoro è un processo single-threaded, è isolata in un contenitore e il suo working set è un grande albero in sola lettura che la cache di pagina deve contenere. Tre conseguenze si trasferiscono direttamente a chiunque costruisca un servizio del genere.

### 10.1 Provisiona prima la memoria, poi i core

Il risultato più forte della matrice è negativo: sotto 16 GiB il numero di core è quasi irrilevante, e solo a 48 GiB le configurazioni da 4, 8 e 16 vCPU si separano davvero (143, 268, 331). Un operatore che legge la regola convenzionale come “aggiungi un core ogni cinque utenti” compra la risorsa sbagliata. Il meccanismo è la cache di pagina condivisa sull'albero di distribuzione, ed è una proprietà della dimensione di TeX Live piuttosto che di qualsiasi front-end particolare.

### 10.2 Il tasso di ammissione è una risorsa, e di solito viene dimenticato

A $$N=1024$$ il nostro server non ha mai mantenuto più di 205 sandbox vive (Figura 7b) anche se ogni richiesta arrivava insieme. La creazione dei container, non la compilazione, era il limite — coerente con studi di misurazione che attribuiscono il costo di avvio dei container all'overhead del runtime piuttosto che alla dimensione dell'immagine \[19, 20]. Un servizio che dimensiona solo CPU e memoria scoprirà che il proprio comportamento a raffica è governato da una quantità che non ha mai misurato. La forma pratica di ciò è la raccomandazione del §8: limita deliberatamente l'ammissione, perché una coda che scegli è meglio di una coda che scopri.

### 10.3 Un sandbox che sopravvive alla sua compilazione invalida il modello

Ogni cifra di capacità qui assume che il contenitore venga creato, esegua una compilazione ed esca — una durata di decine di secondi e un duty cycle vicino a uno solo mentre gira. Due recenti pattern progettuali infrangono quell'assunzione, e la infrangono nello stesso modo.

Il primo è il sandbox persistente per utente. Assegnare a ogni utente un ambiente privato fisso trasforma una pool multiplexata statisticamente in un insieme di prenotazioni: un servizio che potrebbe servire 256 compilazioni concorrenti da 64 core grazie al time-sharing può servire solo 16 utenti se a ciascuno vengono dati quattro core dedicati, un ordine di grandezza in meno per lo stesso hardware. I nostri dati quantificano il costo di quella scelta invece di opporvisi — le prenotazioni acquistano prevedibilità, e il tasso di cambio è circa $$16\times$$ al punto operativo che raccomandiamo.

Il secondo, e più recente, è l'agente IA che condivide il sandbox con il compilatore. Nelle piattaforme di authoring assistite da agenti lo stesso contenitore che esegue XeLaTeX può anche ospitare un agente di coding di lunga durata, quindi è occupato in modo continuo anziché a raffiche. I praticanti riportano esattamente il sintomo che il modello prevede per tali distribuzioni — lentezza sostenuta con conteggi di utenti modesti \[15]. L'interazione merita di essere dichiarata con precisione, perché non è semplicemente “più carico”. Tre dei nostri risultati si compongono. L'occupazione smette di essere a raffica, quindi la legge di time-sharing del §4.2 si applica all'intera popolazione in una volta anziché alla frazione che sta compilando in quel momento. La cache di pagina, che è ciò che produce il ritorno superlineare di memoria del §4.1, è ora condivisa con il working set dell'agente e non rimane più calda per TeX. E il limite di memoria mancante del contenitore di §6.2 diventa molto più pericoloso, perché un contenitore che non termina mai non restituisce mai la sua memoria.

Non abbiamo misurato una tale piattaforma e non facciamo alcuna affermazione su prodotti specifici. Quello che possiamo dire è ciò che i nostri numeri implicano per il design: un'architettura che assegna a ogni utente un sandbox multi-core di lunga durata dovrebbe essere dimensionata come un sistema di prenotazioni, non in base alle cifre di concorrenza riportate qui, e la capacità che può aspettarsi è più vicina al numero di core diviso per i core per utente che a qualsiasi cosa nella Tabella 1.

## 11. Minacce alla validità

### 11.1 Un singolo documento

Tutte le misurazioni usano un documento XeLaTeX di 63 pagine. Le capacità assolute differiranno per altri documenti; le leggi di scaling, che sono rapporti, non dovrebbero differire. Un documento con un resident set sostanzialmente più grande sposterebbe il memory wall senza cambiarne il carattere superlineare.

### 11.2 Host virtualizzato

I guest girano sotto KVM su una sola macchina fisica, quindi i numeri assoluti includono l'overhead di virtualizzazione e i guest condividono una cache di pagina dell'host e un dispositivo NVMe. Abbiamo mitigato il confondente più grande spegnendo i guest non correlati dopo aver osservato che la pressione di memoria dell'host gonfia i load average all'interno del guest di più di $$3\times$$ a parità di concorrenza.

### 11.3 Arrivo simultaneo

Ogni compilazione viene avviata in un istante, che è il caso peggiore. I veri utenti arrivano come processo stocastico, quindi una distribuzione dimensionata secondo i nostri numeri ha margine anziché deficit — ma il picco alla fine di una scadenza di invio è più vicino al nostro modello che a uno di Poisson.

### 11.4 Configurazioni di margine

A 2 GiB il sistema è abbastanza vicino al collasso che esecuzioni ripetute della stessa configurazione possono differire di una compilazione. Riportiamo il valore prudente e non traiamo conclusioni da differenze di $$\pm 1$$ in quel regime.

## 12. Disponibilità

Il sistema sotto test, gli strumenti di distribuzione e il progetto a monte da cui deriva sono tutti pubblici:

* Ayakaleaf Pro — <https://github.com/ayaka-notes/ayakaleaf-pro>
* Toolkit di distribuzione — <https://github.com/ayaka-notes/toolkit>
* Documentazione — [https://ayakaleaf-pro.ayaka.space](https://ayakaleaf-pro.ayaka.space/)
* Overleaf a monte — <https://github.com/overleaf/overleaf>
* Immagini di compilazione TeX Live — `ghcr.io/ayaka-notes/texlive-full:2025.1`

Ogni posizione sorgente che citiamo è fornita come percorso relativo al repository con un numero di riga rispetto ad Ayakaleaf Pro v6.2.2, e i due commit a monte che datiamo (`9a519f0d3d`, `5d472e9b38`) sono raggiungibili nella cronologia di Overleaf.

## 13. Contributi

Musicminion ha progettato lo studio, fornito e gestito i banchi di prova, diretto la linea di indagine e verificato ogni misurazione riportata qui. Claude Opus 5 (Anthropic) ha costruito e gestito l'infrastruttura del benchmark, automatizzato le distribuzioni, svolto l'archeologia del codice sorgente, prodotto le figure e redatto il manoscritto. Entrambi gli autori hanno revisionato il testo finale. Laddove una corsa è riportata come contaminata — il sweep di 1021 sessioni del §4.3 e il livello anomalo di §4.1 — il difetto è stato individuato durante la revisione e la corsa è stata ripetuta prima della pubblicazione anziché essere scartata silenziosamente. $$N=256$$ livello di §4.1 — il difetto è stato individuato durante la revisione e la corsa è stata ripetuta prima della pubblicazione anziché essere scartata silenziosamente.

I lettori dovrebbero notare che le politiche di paternità in ACM, IEEE e ICMJE attualmente riservano la paternità alle parti in grado di assumersi la responsabilità di un lavoro, e richiederebbero che il contributo del secondo autore fosse registrato come disclosure piuttosto che come byline. Dichiaramo qui esplicitamente la divisione del lavoro in modo che il record sia accurato in base a entrambe le convenzioni.

## 14. Conclusione

La pianificazione della capacità per Overleaf auto-ospitato non è una questione di scalare una sola risorsa. Tre risultati dovrebbero cambiarne il modo in cui viene fatta.

Primo, sotto 32 GiB di memoria del guest, il numero di core conta appena: a 16 GiB le capacità dei guest da 4, 8 e 16 vCPU differiscono di meno dell'8%. La memoria, tramite la cache di pagina condivisa sull'albero di TeX Live, stabilisce il limite; i core iniziano a contare solo quando la memoria è abbondante.

Secondo, due parametri software pesano più dell'hardware. Alzare il limite rigido di 65 compilazioni codificato in CLSI e aumentare il timeout di compilazione predefinito di 180 s ha portato un guest da 8 vCPU / 48 GiB da 64 a 268 compilazioni concorrenti — un fattore di 4,2 senza hardware aggiuntivo. Nessuno dei due è individuabile dalla documentazione di configurazione; uno non è configurabile affatto.

Terzo, la domanda “quanti utenti concorrenti supporta questa macchina” è sotto-specificata. La concorrenza in questo sistema è pura time-sharing, e la capacità è quella che il timeout consente. La forma onesta della risposta afferma entrambe le cose: *questa macchina gestisce* $$N$$ *compilazioni simultanee se gli utenti sono disposti ad aspettare* $$T$$ *secondi*, con $$N$$ e $$T$$ correlato dall’Equazione (1).

Segnaliamo anche un difetto latente: il limite di memoria per container nel runner Docker è risultato inefficace dal 2018, sia per entità sia per collocazione. Il suo effetto pratico è che l’esaurimento della memoria in una piccola distribuzione manda giù l’intero servizio invece della singola compilazione responsabile.

## Riferimenti

\[1] Overleaf. *Requisiti hardware*, Documentazione On-premises. <https://docs.overleaf.com/on-premises/getting-started/requirements/hardware-requirements>

\[2] Overleaf. *Scalabilità orizzontale*, Documentazione On-premises. <https://docs.overleaf.com/on-premises/maintenance/horizontal-scaling>

\[3] Overleaf. *Microservizi*, Documentazione On-premises. <https://docs.overleaf.com/on-premises/getting-started/microservices>

\[4] Overleaf. *Repository sorgente*. <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 e D. Lewin. *Hashing coerente e alberi casuali: protocolli di caching distribuito per alleviare i punti caldi sul World Wide Web*. STOC, 1997.

\[8] J. Tan e M. Rigger. *Incoerenze nei documenti prodotti da TeX*. In *Proc. 33rd ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA)*, Vienna, 2024. doi: <https://doi.org/10.1145/3650212.3680370>

\[9] C. A. Ellis e S. J. Gibbs. *Controllo della concorrenza nei sistemi groupware*. In *Proc. ACM SIGMOD*, pp. 399–407, 1989.

\[10] D. A. Nichols, P. Curtis, M. Dixon e J. Lamping. *Finestratura ad alta latenza e bassa banda nel sistema di collaborazione Jupiter*. In *Proc. ACM UIST*, pp. 111–120, 1995.

\[11] M. Shapiro, N. Preguiça, C. Baquero e M. Zawirski. *Tipi di dati replicati privi di conflitti*. In *Proc. SSS*, pp. 386–400, 2011.

\[12] N. J. Gunther. *Pianificazione guerrilla della capacità: un approccio tattico alla pianificazione di applicazioni e servizi altamente scalabili*. Springer, 2007.

\[13] The LaTeX3 Project. *l3build — Un sistema di test e compilazione per (La)TeX*. CTAN.

\[14] M. Isaksson. *Quale sistema di compilazione LaTeX è il più veloce? Un benchmark*. <https://blog.martisak.se/latex-build-systems-comparison/>

\[15] Resoconti di professionisti di una latenza sostenuta in piattaforme di authoring assistite da agenti che collocano un agente di codifica persistente insieme al compilatore LaTeX in un sandbox per utente. Lo citiamo come esperienza operativa riportata, non come misurazione controllata; non abbiamo eseguito benchmark su una piattaforma del genere.

\[16] D. E. Knuth. *Il TeXbook*. Addison-Wesley, 1984.

\[17] G. Lim, M. Ham, J. Moon e W. Song. *LightSys: sistema CI leggero ed efficiente per migliorare la velocità di integrazione del software*. arXiv:2101.07961 \[cs.SE], 2021. Preprint.

\[18] G. Lim, M. Ham, J. Moon, W. Song, S. Woo e S. Oh. *TAOS-CI: sistema di integrazione continua leggero e modulare per l’edge computing*. arXiv:2101.08889 \[cs.SE], 2021. Preprint.

\[19] S. Khan. *Scomporre le prestazioni di avvio dei container Docker: uno studio di misurazione su tre livelli su infrastrutture eterogenee*. arXiv:2602.15214, 2026. Preprint.

\[20] R. Gupta e K. Nahrstedt. *Caratterizzazione delle prestazioni dei container nell’edge computing*. arXiv:2505.02082, 2025. Preprint.

\[21] S. Checkoway, H. Shacham ed E. Rescorla. *I formati di dati solo testo sono sicuri? Oppure, usa questo file di classe LaTeX per compromettere il tuo computer*. In *Proc. USENIX Workshop on Large-Scale Exploits and Emergent Threats (LEET)*, 2010.

\[22] G. Lacombe, K. Masalygina, A. Tahiri, C. Adam e C. Lauradoux. *Puoi accettare file LaTeX da sconosciuti? Dieci anni dopo*. arXiv:2102.00856 \[cs.CR], 2021. Preprint.

\[23] J. D. C. Little. *Una dimostrazione della formula di coda* $$L=\lambda W$$. Operations Research, 9(3):383–387, 1961.

\[24] G. M. Amdahl. *Validità dell’approccio a singolo processore per raggiungere capacità di calcolo su larga scala*. 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/it/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.
