koa-classic-server 2.6.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +101 -0
- package/README.md +564 -591
- package/__tests__/benchmark-results-v3.0.0.txt +372 -0
- package/__tests__/benchmark.js +1 -1
- package/__tests__/caching-headers.test.js +30 -30
- package/__tests__/compression-fixtures/data.json +1 -0
- package/__tests__/compression-fixtures/large.txt +1 -0
- package/__tests__/compression-fixtures/small.txt +1 -0
- package/__tests__/compression.test.js +284 -0
- package/__tests__/customTest/serversToLoad.util.js +5 -5
- package/__tests__/demo-regex-index.js +4 -4
- package/__tests__/deprecation-warnings.test.js +71 -183
- package/__tests__/directory-sorting-links.test.js +1 -1
- package/__tests__/dt-unknown.test.js +39 -28
- package/__tests__/ejs.test.js +1 -1
- package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
- package/__tests__/hidden-fixtures/.env +2 -0
- package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
- package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
- package/__tests__/hidden-fixtures/data.key +1 -0
- package/__tests__/hidden-fixtures/file.secret +1 -0
- package/__tests__/hidden-fixtures/index.html +1 -0
- package/__tests__/hidden-fixtures/normal.txt +1 -0
- package/__tests__/hidden-fixtures/subdir/.env +1 -0
- package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
- package/__tests__/hidden-option.test.js +407 -0
- package/__tests__/hideExtension.test.js +70 -13
- package/__tests__/index-option.test.js +18 -16
- package/__tests__/index.test.js +14 -10
- package/__tests__/listing.test.js +437 -0
- package/__tests__/logger.test.js +232 -0
- package/__tests__/range-fixtures/sample.txt +1 -0
- package/__tests__/range.test.js +223 -0
- package/__tests__/security-headers.test.js +165 -0
- package/__tests__/security.test.js +148 -162
- package/__tests__/server-cache-fixtures/large.txt +1 -0
- package/__tests__/server-cache-fixtures/small.txt +1 -0
- package/__tests__/server-cache.test.js +594 -0
- package/__tests__/symlink.test.js +18 -15
- package/__tests__/template-timeout.test.js +321 -0
- package/docs/ACTION_PLAN.md +293 -0
- package/docs/CHANGELOG.md +289 -0
- package/docs/CODE_REVIEW.md +2 -0
- package/docs/DOCUMENTATION.md +259 -32
- package/docs/EXAMPLES_INDEX_OPTION.md +3 -3
- package/docs/FLOW_DIAGRAM.md +15 -13
- package/docs/INDEX_OPTION_PRIORITY.md +2 -2
- package/docs/OPTIMIZATION_HTTP_CACHING.md +2 -0
- package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
- package/docs/PERFORMANCE_COMPARISON.md +7 -7
- package/docs/security_improvement_for_V3.md +421 -0
- package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +5 -5
- package/docs/template-engine/esempi-incrementali.js +1 -1
- package/eslint.config.mjs +17 -0
- package/index.cjs +1507 -429
- package/index.mjs +1 -5
- package/package.json +9 -1
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
# Performance Optimization Roadmap — koa-classic-server v3.0.0
|
|
2
|
+
|
|
3
|
+
**File:** `docs/OPTIMIZATION_ROADMAP_for_V3.md`
|
|
4
|
+
**Data analisi:** 2026-04-14
|
|
5
|
+
**Versione sorgente analizzata:** branch `claude/koa-v3-preparation-rVXMD`
|
|
6
|
+
**File analizzato:** `index.cjs` (1544 righe)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Indice
|
|
11
|
+
|
|
12
|
+
1. [Introduzione](#1-introduzione)
|
|
13
|
+
- 1.1 Scopo del documento
|
|
14
|
+
- 1.2 Come usare questo roadmap
|
|
15
|
+
- 1.3 Legenda (impatto / difficoltà / rischio)
|
|
16
|
+
2. [Riepilogo generale](#2-riepilogo-generale)
|
|
17
|
+
- 2.1 Tabella dei 15 punti
|
|
18
|
+
- 2.2 Mappa visiva fase → punti → impatto
|
|
19
|
+
3. [FASE 1 — Precomputation al factory time](#3-fase-1--precomputation-al-factory-time)
|
|
20
|
+
- [x] 3.1 #2 — `urlPrefix.split()` precompilato
|
|
21
|
+
- [x] 3.2 #11 — `require('stream')` → top-level
|
|
22
|
+
- [x] 3.3 #12 — `Math.log(1024)` → costante di modulo
|
|
23
|
+
- [x] 3.4 #14 — `for...in` su array → loop indicizzato
|
|
24
|
+
- [x] 3.5 #15 — `pageHref.origin+pathname` estratto prima del loop
|
|
25
|
+
4. [FASE 2 — Riduzione costruzioni new URL()](#4-fase-2--riduzione-costruzioni-new-url)
|
|
26
|
+
- [x] 4.1 #1a — `new URL()` a riga 744 incondizionata
|
|
27
|
+
- [x] 4.2 #1b — Estrazione `_origin` per evitare concatenazioni ripetute
|
|
28
|
+
5. [FASE 3 — Helper puri → scope modulo + string single-pass](#5-fase-3--helper-puri--scope-modulo--string-single-pass)
|
|
29
|
+
- [x] 5.1 #8 — `escapeHtml` e `formatSize` → scope modulo
|
|
30
|
+
- [x] 5.2 #9 — `escapeHtml`: 5 `replace()` → regex single-pass + lookup table
|
|
31
|
+
- [x] 5.3 #10 — HTML 404 → costante pre-calcolata
|
|
32
|
+
- [x] 5.4 #13 — `item[sy_type]` Symbol hack → API dirent ufficiale
|
|
33
|
+
6. [FASE 4 — Strutture dati: Array → Set](#6-fase-4--strutture-dati-array--set)
|
|
34
|
+
- [x] 6.1 #7 — `mimeTypes` `Array.includes()` O(n) → `Set.has()` O(1)
|
|
35
|
+
7. [FASE 5 — Directory listing: I/O parallelo](#7-fase-5--directory-listing-io-parallelo)
|
|
36
|
+
- [x] 7.1 #4 — Eliminare doppia `stat()` per symlink
|
|
37
|
+
- [x] 7.2 #3 — Loop `for...of` `await` → `Promise.all` per stat parallele
|
|
38
|
+
8. [FASE 6 — findIndexFile fast-path](#8-fase-6--findindexfile-fast-path)
|
|
39
|
+
- [x] 8.1 #5 — `stat()` diretto per pattern stringa, `readdir` solo per RegExp
|
|
40
|
+
9. [FASE 7 — LFU cache: eviction O(1)](#9-fase-7--lfu-cache-eviction-o1)
|
|
41
|
+
- [x] 9.1 #6 — Struttura LFU classica con bucket di frequenza
|
|
42
|
+
10. [Stima impatto complessivo atteso](#10-stima-impatto-complessivo-atteso)
|
|
43
|
+
- 10.1 File serving (rawFile cache warm)
|
|
44
|
+
- 10.2 Directory listing su disco locale
|
|
45
|
+
- 10.3 Directory listing su NFS/SMB
|
|
46
|
+
- 10.4 High-throughput request rate
|
|
47
|
+
11. [Note implementative e ordine consigliato](#11-note-implementative-e-ordine-consigliato)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 1. Introduzione
|
|
52
|
+
|
|
53
|
+
### 1.1 Scopo del documento
|
|
54
|
+
|
|
55
|
+
Questo documento raccoglie tutte le opportunità di ottimizzazione delle performance
|
|
56
|
+
identificate nel sorgente di koa-classic-server durante la preparazione della v3.0.0.
|
|
57
|
+
Le ottimizzazioni sono organizzate in 7 fasi ordinate per facilità di implementazione
|
|
58
|
+
e impatto, con checkbox per tracciare l'avanzamento.
|
|
59
|
+
|
|
60
|
+
### 1.2 Come usare questo roadmap
|
|
61
|
+
|
|
62
|
+
- `[ ]` = da fare
|
|
63
|
+
- `[x]` = completato
|
|
64
|
+
- Ogni punto riporta: descrizione del problema, riga sorgente, causa root, fix proposto
|
|
65
|
+
- Le fasi sono indipendenti tra loro e possono essere eseguite in qualsiasi ordine,
|
|
66
|
+
ma l'ordine proposto minimizza il rischio di regressioni
|
|
67
|
+
|
|
68
|
+
### 1.3 Legenda
|
|
69
|
+
|
|
70
|
+
| Simbolo | Significato |
|
|
71
|
+
|---------|-------------|
|
|
72
|
+
| 🔴 Alto | Impatto misurabile su ogni richiesta o su listing di directory |
|
|
73
|
+
| 🟡 Medio | Impatto visibile sotto carico o con directory grandi |
|
|
74
|
+
| 🟢 Basso | Micro-ottimizzazione, guadagno marginale |
|
|
75
|
+
| ⚙️ Qualità | Migliora robustezza/leggibilità anche senza impatto diretto |
|
|
76
|
+
| ★ Difficoltà Bassa | < 1 ora, modifica localizzata, nessun rischio architetturale |
|
|
77
|
+
| ★★ Difficoltà Media | Richiede refactor di una funzione, test di verifica necessari |
|
|
78
|
+
| ★★★ Difficoltà Alta | Cambiamento algoritmico, struttura dati nuova, test estensivi |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 2. Riepilogo generale
|
|
83
|
+
|
|
84
|
+
### 2.1 Tabella dei 15 punti
|
|
85
|
+
|
|
86
|
+
| # | Ottimizzazione | Riga/i | Fase | Impatto | Difficoltà | Rischio |
|
|
87
|
+
|---|---------------|--------|------|---------|------------|---------|
|
|
88
|
+
| 1a | `new URL()` a riga 744 — incondizionata | 744 | 2 | 🔴 Alto | ★ | Basso |
|
|
89
|
+
| 1b | `ctx.protocol+'://'+ctx.host` ripetuto 3+ volte | 660,744,756 | 2 | 🔴 Alto | ★ | Basso |
|
|
90
|
+
| 2 | `urlPrefix.split("/")` ad ogni richiesta | 670 | 1 | 🔴 Alto | ★ | Basso |
|
|
91
|
+
| 3 | `show_dir`: stat serializzate (`await` in `for...of`) | 1371–1430 | 5 | 🔴 Alto | ★★ | Basso |
|
|
92
|
+
| 4 | `show_dir`: doppia `stat()` per symlink | 1396–1420 | 5 | 🔴 Alto | ★ | Basso |
|
|
93
|
+
| 5 | `findIndexFile`: readdir + stat tutti + stat secondo | 859–908 | 6 | 🟡 Medio | ★★ | Medio |
|
|
94
|
+
| 6 | LFU eviction: scansione lineare O(n) | 577–597 | 7 | 🟡 Medio | ★★★ | Medio |
|
|
95
|
+
| 7 | `mimeTypes.includes()`: Array O(n) vs Set O(1) | 1105 | 4 | 🟡 Medio | ★ | Basso |
|
|
96
|
+
| 8 | `escapeHtml`, `formatSize` ricreate ad ogni richiesta | 1280,1532 | 3 | 🟡 Medio | ★ | Basso |
|
|
97
|
+
| 9 | `escapeHtml`: 5 `.replace()` in catena | 1536–1541 | 3 | 🟡 Medio | ★ | Basso |
|
|
98
|
+
| 10 | `requestedUrlNotFound()` rigenera HTML ad ogni 404 | 917–933 | 3 | 🟢 Basso | ★ | Basso |
|
|
99
|
+
| 11 | `require('stream')` dentro l'handler | 1234 | 1 | 🟢 Basso | ★ | Basso |
|
|
100
|
+
| 12 | `formatSize`: `Math.log(1024)` ricalcolato ad ogni call | 1285 | 1 | 🟢 Basso | ★ | Basso |
|
|
101
|
+
| 13 | `item[sy_type]` Symbol interno invece di API dirent | 1366–1373 | 3 | ⚙️ Qualità | ★ | Basso |
|
|
102
|
+
| 14 | `for...in` su array invece di loop indicizzato | 672 | 1 | 🟢 Basso | ★ | Basso |
|
|
103
|
+
| 15 | `pageHref.origin+pathname` ricalcolato nel loop | 1383–1387 | 1 | 🟢 Basso | ★ | Basso |
|
|
104
|
+
|
|
105
|
+
### 2.2 Mappa fase → punti → impatto
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
FASE 1 ── Precomputation ──────── #2 #11 #12 #14 #15 ── ★ ── Medio
|
|
109
|
+
FASE 2 ── URL reduction ──────── #1a #1b ──────────── ★ ── Alto
|
|
110
|
+
FASE 3 ── Helper scope+string ── #8 #9 #10 #13 ──── ★ ── Medio
|
|
111
|
+
FASE 4 ── Array → Set ─────────── #7 ─────────────────── ★ ── Medio
|
|
112
|
+
FASE 5 ── I/O parallelo ────────── #4 #3 ─────────────── ★★ ── Alto ◄ impatto massimo
|
|
113
|
+
FASE 6 ── findIndexFile ────────── #5 ─────────────────── ★★ ── Medio
|
|
114
|
+
FASE 7 ── LFU O(1) ─────────────── #6 ─────────────────── ★★★ ── Medio
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 3. FASE 1 — Precomputation al factory time
|
|
120
|
+
|
|
121
|
+
**Concetto:** tutto ciò che viene calcolato inutilmente ad ogni richiesta
|
|
122
|
+
ma il cui valore non cambia mai dopo l'inizializzazione del middleware.
|
|
123
|
+
Queste modifiche sono le più sicure: localizzate, senza effetti collaterali,
|
|
124
|
+
verificabili con i test esistenti senza aggiungerne di nuovi.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### [x] 3.1 — #2: `urlPrefix.split("/")` precompilato
|
|
129
|
+
|
|
130
|
+
**Riga:** `670`
|
|
131
|
+
**Problema:**
|
|
132
|
+
```js
|
|
133
|
+
// ATTUALE — eseguito ad ogni richiesta
|
|
134
|
+
const a_urlPrefix = options.urlPrefix.split("/");
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`options.urlPrefix` non cambia mai dopo la factory. La chiamata `.split()` alloca
|
|
138
|
+
un nuovo Array ad ogni richiesta.
|
|
139
|
+
|
|
140
|
+
**Fix:**
|
|
141
|
+
```js
|
|
142
|
+
// In factory (una volta sola):
|
|
143
|
+
const _urlPrefixParts = options.urlPrefix.split("/");
|
|
144
|
+
|
|
145
|
+
// Nell'handler — sostituire a_urlPrefix con _urlPrefixParts
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### [x] 3.2 — #11: `require('stream')` → top-level
|
|
151
|
+
|
|
152
|
+
**Riga:** `1234`
|
|
153
|
+
**Problema:**
|
|
154
|
+
```js
|
|
155
|
+
// ATTUALE — dentro il corpo della richiesta, ramo streaming+rawBuffer
|
|
156
|
+
const { Readable } = require('stream');
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Node.js cache i moduli, ma la chiamata `require()` è comunque una Map lookup +
|
|
160
|
+
destructuring ad ogni esecuzione del branch. Appartiene al top del file.
|
|
161
|
+
|
|
162
|
+
**Fix:**
|
|
163
|
+
```js
|
|
164
|
+
// Aggiungere in cima al file, dopo gli altri require:
|
|
165
|
+
const { Readable } = require('stream');
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### [x] 3.3 — #12: `Math.log(1024)` → costante di modulo
|
|
171
|
+
|
|
172
|
+
**Riga:** `1285`
|
|
173
|
+
**Problema:**
|
|
174
|
+
```js
|
|
175
|
+
function formatSize(bytes) {
|
|
176
|
+
const k = 1024;
|
|
177
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k)); // Math.log(k) ogni volta
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`Math.log(1024)` è una costante pura. Viene ricalcolata ad ogni chiamata a
|
|
181
|
+
`formatSize()`, che viene invocata per ogni entry visibile in ogni listing.
|
|
182
|
+
|
|
183
|
+
**Fix:**
|
|
184
|
+
```js
|
|
185
|
+
// A scope di modulo:
|
|
186
|
+
const _LOG_1024 = Math.log(1024);
|
|
187
|
+
|
|
188
|
+
// In formatSize:
|
|
189
|
+
const i = Math.floor(Math.log(bytes) / _LOG_1024);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### [x] 3.4 — #14: `for...in` su array → loop indicizzato
|
|
195
|
+
|
|
196
|
+
**Riga:** `672`
|
|
197
|
+
**Problema:**
|
|
198
|
+
```js
|
|
199
|
+
// ATTUALE
|
|
200
|
+
for (const key in a_urlPrefix) {
|
|
201
|
+
if (a_urlPrefix[key] !== a_pathname[key]) {
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`for...in` su Array itera sulle chiavi come stringhe (`"0"`, `"1"`, ...) e in
|
|
205
|
+
teoria include proprietà prototipo non standard. La semantica corretta per
|
|
206
|
+
iterare un array con indice è il loop indicizzato.
|
|
207
|
+
|
|
208
|
+
**Fix:**
|
|
209
|
+
```js
|
|
210
|
+
for (let i = 0; i < _urlPrefixParts.length; i++) {
|
|
211
|
+
if (_urlPrefixParts[i] !== a_pathname[i]) {
|
|
212
|
+
await next();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> Dipende dal completamento del punto 3.1 — `_urlPrefixParts` già precompilato.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### [x] 3.5 — #15: `pageHref.origin + pageHref.pathname` estratto prima del loop
|
|
223
|
+
|
|
224
|
+
**Righe:** `1383–1387`
|
|
225
|
+
**Problema:**
|
|
226
|
+
```js
|
|
227
|
+
// ATTUALE — dentro il for...of, ri-calcolata per ogni item
|
|
228
|
+
const baseUrl = pageHref.origin + pageHref.pathname;
|
|
229
|
+
if (baseUrl === pageHref.origin + options.urlPrefix + "/" || ...) {
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
La stringa `pageHref.origin + pageHref.pathname` è identica per tutti gli item
|
|
233
|
+
della stessa directory listing. Viene ricostruita (e confrontata con altre
|
|
234
|
+
concatenazioni) per ogni elemento del loop.
|
|
235
|
+
|
|
236
|
+
> **Nota:** c'è anche un conflitto di nome — `baseUrl` alla riga 1383 ombreggia
|
|
237
|
+
> `baseUrl` dichiarata alla riga 1322 per i link di sorting.
|
|
238
|
+
|
|
239
|
+
**Fix:**
|
|
240
|
+
```js
|
|
241
|
+
// Prima del for...of, dopo il calcolo di dirRelPath:
|
|
242
|
+
const _listingBaseUrl = pageHref.origin + pageHref.pathname;
|
|
243
|
+
const _listingOriginPrefix = pageHref.origin + options.urlPrefix;
|
|
244
|
+
|
|
245
|
+
// Nel loop, sostituire le concatenazioni ripetute con le variabili pre-calcolate.
|
|
246
|
+
// Rinominare anche la variabile locale di sorting in sortBaseUrl per eliminare
|
|
247
|
+
// il conflitto di nome.
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 4. FASE 2 — Riduzione costruzioni `new URL()`
|
|
253
|
+
|
|
254
|
+
**Concetto:** il costruttore `URL` è significativamente più costoso di operazioni
|
|
255
|
+
stringa equivalenti. Fa parsing completo, validazione RFC 3986, canonicalizzazione
|
|
256
|
+
e allocazione di un oggetto con molte proprietà. Ogni richiesta subisce almeno
|
|
257
|
+
una costruzione; in certi path ne subisce 3–4.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### [x] 4.1 — #1a: `new URL()` a riga 744 — incondizionata anche senza `hideExtension`
|
|
262
|
+
|
|
263
|
+
**Riga:** `744`
|
|
264
|
+
**Problema:**
|
|
265
|
+
```js
|
|
266
|
+
// ATTUALE — eseguito su OGNI richiesta, indipendentemente da hideExtension
|
|
267
|
+
const originalUrlPath = new URL(ctx.protocol + '://' + ctx.host + urlToUse).pathname;
|
|
268
|
+
const hadTrailingSlash = originalUrlPath.length > 1 && originalUrlPath.endsWith('/');
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Questo URL viene costruito esclusivamente per determinare `hadTrailingSlash`,
|
|
272
|
+
informazione usata solo dentro il blocco `if (options.hideExtension)`.
|
|
273
|
+
Se `hideExtension` non è configurato (caso più comune), l'intera costruzione
|
|
274
|
+
è sprecata.
|
|
275
|
+
|
|
276
|
+
**Fix:**
|
|
277
|
+
```js
|
|
278
|
+
// Spostare le due righe DENTRO il blocco:
|
|
279
|
+
if (options.hideExtension) {
|
|
280
|
+
const originalUrlPath = new URL(ctx.protocol + '://' + ctx.host + urlToUse).pathname;
|
|
281
|
+
const hadTrailingSlash = originalUrlPath.length > 1 && originalUrlPath.endsWith('/');
|
|
282
|
+
// ... resto della logica hideExtension
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Alternativa zero-URL per il trailing slash:**
|
|
287
|
+
```js
|
|
288
|
+
// Senza costruire URL, direttamente sulla stringa:
|
|
289
|
+
const rawPath = urlToUse.split('?')[0]; // rimuove query string
|
|
290
|
+
const hadTrailingSlash = rawPath.length > 1 && rawPath.endsWith('/');
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
### [x] 4.2 — #1b: `ctx.protocol + '://' + ctx.host` ripetuto 3+ volte
|
|
296
|
+
|
|
297
|
+
**Righe:** `660, 744, 756`
|
|
298
|
+
**Problema:**
|
|
299
|
+
```js
|
|
300
|
+
const fullUrl = ctx.protocol + '://' + ctx.host + urlToUse; // riga 660
|
|
301
|
+
const originalUrlPath = new URL(ctx.protocol + '://' + ctx.host + urlToUse)...; // riga 744
|
|
302
|
+
const originalUrlObj = new URL(ctx.protocol + '://' + ctx.host + ctx.originalUrl); // riga 756
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
La stringa base `ctx.protocol + '://' + ctx.host` viene concatenata 3 volte
|
|
306
|
+
per richiesta, ognuna producendo una stringa temporanea intermedia.
|
|
307
|
+
|
|
308
|
+
**Fix:**
|
|
309
|
+
```js
|
|
310
|
+
// All'inizio dell'handler, una sola volta:
|
|
311
|
+
const _origin = ctx.protocol + '://' + ctx.host;
|
|
312
|
+
|
|
313
|
+
// Poi ovunque:
|
|
314
|
+
const fullUrl = _origin + urlToUse;
|
|
315
|
+
const originalUrlObj = new URL(_origin + ctx.originalUrl);
|
|
316
|
+
// ecc.
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 5. FASE 3 — Helper puri → scope modulo + string single-pass
|
|
322
|
+
|
|
323
|
+
**Concetto:** funzioni che non catturano variabili di closure ma sono dichiarate
|
|
324
|
+
dentro l'handler (o dentro `show_dir` che è dentro l'handler), venendo ricreate
|
|
325
|
+
come nuovi oggetti-funzione ad ogni richiesta o ad ogni listing. Spostare le
|
|
326
|
+
funzioni pure a scope di modulo elimina questa allocazione sistematica.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
### [x] 5.1 — #8: `escapeHtml` e `formatSize` → scope di modulo
|
|
331
|
+
|
|
332
|
+
**Righe:** `1280` (formatSize), `1532` (escapeHtml)
|
|
333
|
+
**Problema:** entrambe le funzioni sono dichiarate dentro la closure dell'handler.
|
|
334
|
+
Non usano alcuna variabile di closure: sono funzioni pure che dipendono solo
|
|
335
|
+
dai propri argomenti.
|
|
336
|
+
|
|
337
|
+
`escapeHtml` viene chiamata per ogni entry di ogni listing. Se una directory
|
|
338
|
+
ha 200 file, la funzione viene creata una volta per listing ma chiamata 200+
|
|
339
|
+
volte con un oggetto-funzione nato e morto insieme alla richiesta.
|
|
340
|
+
|
|
341
|
+
**Fix:** spostare entrambe le definizioni a livello di `module.exports`
|
|
342
|
+
(scope di modulo), prima della `return async (ctx, next) => {`.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
### [x] 5.2 — #9: `escapeHtml` single-pass con lookup table
|
|
347
|
+
|
|
348
|
+
**Righe:** `1536–1541`
|
|
349
|
+
**Problema:**
|
|
350
|
+
```js
|
|
351
|
+
// ATTUALE — 5 passaggi sulla stringa, 4 stringhe intermedie allocate
|
|
352
|
+
return unsafe
|
|
353
|
+
.replace(/&/g, "&")
|
|
354
|
+
.replace(/</g, "<")
|
|
355
|
+
.replace(/>/g, ">")
|
|
356
|
+
.replace(/"/g, """)
|
|
357
|
+
.replace(/'/g, "'");
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Fix:**
|
|
361
|
+
```js
|
|
362
|
+
// A scope di modulo (una sola volta):
|
|
363
|
+
const _HTML_ESCAPE_MAP = {
|
|
364
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
365
|
+
};
|
|
366
|
+
const _HTML_ESCAPE_RE = /[&<>"']/g;
|
|
367
|
+
|
|
368
|
+
// Funzione:
|
|
369
|
+
function escapeHtml(unsafe) {
|
|
370
|
+
if (typeof unsafe !== 'string') return unsafe;
|
|
371
|
+
return unsafe.replace(_HTML_ESCAPE_RE, c => _HTML_ESCAPE_MAP[c]);
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Un solo passaggio, una sola stringa allocata, regex compilata una volta.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
### [x] 5.3 — #10: HTML 404 → costante pre-calcolata
|
|
380
|
+
|
|
381
|
+
**Righe:** `917–933`
|
|
382
|
+
**Problema:**
|
|
383
|
+
```js
|
|
384
|
+
function requestedUrlNotFound() {
|
|
385
|
+
return `
|
|
386
|
+
<!DOCTYPE html>
|
|
387
|
+
<html>...
|
|
388
|
+
</html>
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Il template literal produce ogni volta una stringa identica. La funzione viene
|
|
394
|
+
chiamata ad ogni 404, ad ogni hidden check, ad ogni path traversal bloccato.
|
|
395
|
+
|
|
396
|
+
**Fix:**
|
|
397
|
+
```js
|
|
398
|
+
// A scope di factory (calcolato una volta):
|
|
399
|
+
const _NOT_FOUND_HTML = `<!DOCTYPE html>
|
|
400
|
+
<html>
|
|
401
|
+
<head>
|
|
402
|
+
<meta charset="UTF-8">
|
|
403
|
+
<title>URL not found</title>
|
|
404
|
+
</head>
|
|
405
|
+
<body>
|
|
406
|
+
<h1>Not Found</h1>
|
|
407
|
+
<h3>The requested URL was not found on this server.</h3>
|
|
408
|
+
</body>
|
|
409
|
+
</html>`;
|
|
410
|
+
|
|
411
|
+
// sendNotFound diventa:
|
|
412
|
+
function sendNotFound(ctx) {
|
|
413
|
+
setGeneratedPageHeaders(ctx, NOT_FOUND_CSP);
|
|
414
|
+
ctx.status = 404;
|
|
415
|
+
ctx.body = _NOT_FOUND_HTML;
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
### [x] 5.4 — #13: `item[sy_type]` Symbol interno → API dirent ufficiale
|
|
422
|
+
|
|
423
|
+
**Righe:** `1366–1373`
|
|
424
|
+
**Problema:**
|
|
425
|
+
```js
|
|
426
|
+
// ATTUALE — accesso a Symbol interno non documentato
|
|
427
|
+
let a_sy = Object.getOwnPropertySymbols(dir[0]);
|
|
428
|
+
const sy_type = a_sy[0];
|
|
429
|
+
// ...
|
|
430
|
+
const type = item[sy_type]; // usato per ogni item
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Questa tecnica legge la proprietà numerica del tipo dirent (`0=DT_UNKNOWN`,
|
|
434
|
+
`1=file`, `2=dir`, `3=symlink`) tramite un Symbol privato di Node.js che
|
|
435
|
+
potrebbe cambiare in versioni future. L'API pubblica equivalente esiste:
|
|
436
|
+
`dirent.isFile()`, `dirent.isDirectory()`, `dirent.isSymbolicLink()`.
|
|
437
|
+
|
|
438
|
+
**Fix:**
|
|
439
|
+
```js
|
|
440
|
+
// Helper a scope di modulo:
|
|
441
|
+
function getDirentType(dirent) {
|
|
442
|
+
if (dirent.isFile()) return 1;
|
|
443
|
+
if (dirent.isDirectory()) return 2;
|
|
444
|
+
if (dirent.isSymbolicLink()) return 3;
|
|
445
|
+
return 0; // DT_UNKNOWN
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Nel loop show_dir, sostituire:
|
|
449
|
+
// const type = item[sy_type];
|
|
450
|
+
// con:
|
|
451
|
+
const type = getDirentType(item);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Eliminare anche le righe `Object.getOwnPropertySymbols` e `sy_type`.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 6. FASE 4 — Strutture dati: Array → Set
|
|
459
|
+
|
|
460
|
+
**Concetto:** sostituire ricerche lineari O(n) con lookup O(1) dove il set
|
|
461
|
+
di valori è fisso e costruito una volta sola all'inizializzazione.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
### [x] 6.1 — #7: `mimeTypes` Array → Set
|
|
466
|
+
|
|
467
|
+
**Riga:** `1105`
|
|
468
|
+
**Problema:**
|
|
469
|
+
```js
|
|
470
|
+
// ATTUALE — Array.includes() O(n), chiamato ad ogni richiesta non-Range
|
|
471
|
+
const isCompressibleMime = compressionConfig.mimeTypes.includes(mimeType);
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
La lista default ha 11 MIME type. Worst case: 11 confronti string ad ogni
|
|
475
|
+
richiesta che potrebbe essere compressa.
|
|
476
|
+
|
|
477
|
+
**Fix:** in `normalizeCompressionConfig()`, restituire un `Set` invece di un `Array`:
|
|
478
|
+
```js
|
|
479
|
+
// ATTUALE
|
|
480
|
+
return { enabled, encodings, minSize, mimeTypes };
|
|
481
|
+
|
|
482
|
+
// FIX
|
|
483
|
+
return { enabled, encodings, minSize, mimeTypes: new Set(mimeTypes) };
|
|
484
|
+
|
|
485
|
+
// Nell'handler, aggiornare la call-site:
|
|
486
|
+
const isCompressibleMime = compressionConfig.mimeTypes.has(mimeType);
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
> **Attenzione:** aggiornare anche il test `compression.test.js` se verifica
|
|
490
|
+
> il tipo di `mimeTypes` direttamente.
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## 7. FASE 5 — Directory listing: I/O parallelo
|
|
495
|
+
|
|
496
|
+
**Concetto:** il bottleneck più impattante del middleware. Le `stat` del filesystem
|
|
497
|
+
durante la generazione del listing vengono eseguite in modo sequenziale (`await`
|
|
498
|
+
in `for...of`), serializzando operazioni che potrebbero essere tutte concorrenti.
|
|
499
|
+
Su NFS o filesystem di rete l'impatto è ordini di grandezza.
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
### [x] 7.1 — #4: Eliminare doppia `stat()` per symlink — riutilizzare `realStat`
|
|
504
|
+
|
|
505
|
+
**Righe:** `1396–1420`
|
|
506
|
+
**Problema:** per ogni entry che è un symlink o `DT_UNKNOWN`, la funzione fa
|
|
507
|
+
due chiamate `stat` al filesystem separate sullo stesso path:
|
|
508
|
+
```js
|
|
509
|
+
// STAT #1 — per determinare il tipo effettivo (riga 1396)
|
|
510
|
+
const realStat = await fs.promises.stat(itemPath);
|
|
511
|
+
if (realStat.isFile()) effectiveType = 1;
|
|
512
|
+
// realStat.size è disponibile ma IGNORATO
|
|
513
|
+
|
|
514
|
+
// ... hidden check ...
|
|
515
|
+
|
|
516
|
+
// STAT #2 — per ottenere la size (riga 1420) — RIDONDANTE per symlink
|
|
517
|
+
const itemStat = await fs.promises.stat(itemPath);
|
|
518
|
+
if (effectiveType === 1) {
|
|
519
|
+
sizeBytes = itemStat.size;
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**Fix:** conservare `realStat` e riutilizzarlo per la size:
|
|
524
|
+
```js
|
|
525
|
+
let cachedStat = null;
|
|
526
|
+
|
|
527
|
+
if (type === 3 || type === 0) {
|
|
528
|
+
try {
|
|
529
|
+
cachedStat = await fs.promises.stat(itemPath); // conservato
|
|
530
|
+
if (cachedStat.isFile()) effectiveType = 1;
|
|
531
|
+
else if (cachedStat.isDirectory()) effectiveType = 2;
|
|
532
|
+
} catch { ... }
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Per la size: riutilizzare cachedStat se disponibile
|
|
536
|
+
if (!isBrokenSymlink) {
|
|
537
|
+
try {
|
|
538
|
+
const itemStat = cachedStat || await fs.promises.stat(itemPath);
|
|
539
|
+
if (effectiveType === 1) {
|
|
540
|
+
sizeBytes = itemStat.size;
|
|
541
|
+
sizeStr = formatSize(sizeBytes);
|
|
542
|
+
}
|
|
543
|
+
} catch { sizeStr = '-'; }
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
### [x] 7.2 — #3: Loop `for...of` con `await` → `Promise.all` per stat parallele
|
|
550
|
+
|
|
551
|
+
**Righe:** `1371–1430`
|
|
552
|
+
**Problema:** il loop raccoglie i dati di ogni entry in modo sequenziale.
|
|
553
|
+
Ogni `await fs.promises.stat()` blocca l'iterazione finché il filesystem
|
|
554
|
+
non risponde. Con N file:
|
|
555
|
+
|
|
556
|
+
- Disco locale (`stat` ≈ 0.3 ms): 100 file → ~30 ms sequenziali vs ~2 ms paralleli
|
|
557
|
+
- NFS (`stat` ≈ 15 ms): 100 file → ~1500 ms sequenziali vs ~20 ms paralleli
|
|
558
|
+
|
|
559
|
+
**Struttura del fix:** separare la raccolta dati (tutte le stat in parallelo)
|
|
560
|
+
dalla generazione HTML (puramente sincrona):
|
|
561
|
+
|
|
562
|
+
```js
|
|
563
|
+
// FASE A: raccogliere tutti i dati in parallelo
|
|
564
|
+
const rawItems = await Promise.all(
|
|
565
|
+
dir.map(async (item) => {
|
|
566
|
+
const type = getDirentType(item); // dopo fix #13
|
|
567
|
+
if (type !== 0 && type !== 1 && type !== 2 && type !== 3) return null;
|
|
568
|
+
|
|
569
|
+
const s_name = item.name;
|
|
570
|
+
const itemPath = path.join(toOpen, s_name);
|
|
571
|
+
|
|
572
|
+
let effectiveType = type;
|
|
573
|
+
let isBrokenSymlink = false;
|
|
574
|
+
let cachedStat = null;
|
|
575
|
+
|
|
576
|
+
// Stat #1: risolvi symlink / DT_UNKNOWN
|
|
577
|
+
if (type === 3 || type === 0) {
|
|
578
|
+
try {
|
|
579
|
+
cachedStat = await fs.promises.stat(itemPath);
|
|
580
|
+
if (cachedStat.isFile()) effectiveType = 1;
|
|
581
|
+
else if (cachedStat.isDirectory()) effectiveType = 2;
|
|
582
|
+
} catch {
|
|
583
|
+
if (type === 3) isBrokenSymlink = true;
|
|
584
|
+
else return null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Hidden check: scarta subito le entry nascoste
|
|
589
|
+
const itemIsDir = effectiveType === 2;
|
|
590
|
+
const itemRelPath = dirRelPath ? dirRelPath + '/' + s_name : s_name;
|
|
591
|
+
if (isHiddenEntry(s_name, itemRelPath, itemIsDir)) return null;
|
|
592
|
+
|
|
593
|
+
// Stat #2: size (riutilizza cachedStat se già disponibile)
|
|
594
|
+
let sizeBytes = 0;
|
|
595
|
+
let sizeStr = '-';
|
|
596
|
+
if (!isBrokenSymlink) {
|
|
597
|
+
try {
|
|
598
|
+
const itemStat = cachedStat || await fs.promises.stat(itemPath);
|
|
599
|
+
if (effectiveType === 1) {
|
|
600
|
+
sizeBytes = itemStat.size;
|
|
601
|
+
sizeStr = formatSize(sizeBytes);
|
|
602
|
+
}
|
|
603
|
+
} catch { sizeStr = '-'; }
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return { name: s_name, type, effectiveType, isBrokenSymlink,
|
|
607
|
+
sizeBytes, sizeStr, itemPath };
|
|
608
|
+
})
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
// FASE B: filtrare null e calcolare campi derivati (pura, sincrona)
|
|
612
|
+
const items = rawItems
|
|
613
|
+
.filter(Boolean)
|
|
614
|
+
.map(item => ({
|
|
615
|
+
...item,
|
|
616
|
+
isSymlink: item.type === 3,
|
|
617
|
+
mimeType: item.effectiveType === 2
|
|
618
|
+
? 'DIR'
|
|
619
|
+
: (mime.lookup(item.itemPath) || 'unknown'),
|
|
620
|
+
itemUri: buildItemUri(item.name),
|
|
621
|
+
isReserved: /* ... */
|
|
622
|
+
}));
|
|
623
|
+
|
|
624
|
+
// FASE C: sort + HTML generation (invariati)
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
> **Attenzione:** `Promise.all` esegue tutte le stat concorrentemente, ma le
|
|
628
|
+
> entry risultanti sono nell'ordine originale del `readdir`. Il sort successivo
|
|
629
|
+
> rimane invariato.
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## 8. FASE 6 — `findIndexFile` fast-path
|
|
634
|
+
|
|
635
|
+
**Concetto:** il caso di gran lunga più comune è un pattern stringa (es. `"index.html"`).
|
|
636
|
+
Invece di leggere tutti i file della directory, verificare e poi cercare, è più
|
|
637
|
+
efficiente tentare una `stat` diretta sul file candidato.
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
### [x] 8.1 — #5: `stat()` diretto per pattern stringa, `readdir` solo per RegExp
|
|
642
|
+
|
|
643
|
+
**Righe:** `859–908`
|
|
644
|
+
**Problema attuale:**
|
|
645
|
+
|
|
646
|
+
1. `readdir()` di tutti i file della directory
|
|
647
|
+
2. `Promise.all` con `isFileOrSymlinkToFile` su tutti i file (`stat` per symlink)
|
|
648
|
+
3. Filtraggio + ricerca del pattern nella lista
|
|
649
|
+
4. Seconda `stat()` sul file trovato per restituire `fileStat`
|
|
650
|
+
|
|
651
|
+
Per una directory con 500 file e pattern `"index.html"`, questo compie centinaia
|
|
652
|
+
di operazioni inutili.
|
|
653
|
+
|
|
654
|
+
**Fix — fast-path per pattern stringa:**
|
|
655
|
+
```js
|
|
656
|
+
async function findIndexFile(dirPath, indexPatterns) {
|
|
657
|
+
for (const pattern of indexPatterns) {
|
|
658
|
+
|
|
659
|
+
// FAST PATH: pattern stringa → stat() diretto, nessun readdir
|
|
660
|
+
if (typeof pattern === 'string') {
|
|
661
|
+
const candidate = path.join(dirPath, pattern);
|
|
662
|
+
try {
|
|
663
|
+
const fileStat = await fs.promises.stat(candidate);
|
|
664
|
+
if (fileStat.isFile()) {
|
|
665
|
+
return { name: pattern, stat: fileStat };
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
continue; // file non esiste, prova il pattern successivo
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// SLOW PATH: pattern RegExp → readdir necessario (comportamento attuale)
|
|
673
|
+
if (pattern instanceof RegExp) {
|
|
674
|
+
try {
|
|
675
|
+
const files = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
676
|
+
// ... logica attuale per RegExp ...
|
|
677
|
+
} catch (error) {
|
|
678
|
+
console.error('Error finding index file:', error);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
> Se tutti i pattern sono stringhe (caso tipico), `readdir` non viene mai chiamato.
|
|
687
|
+
> Se l'array misto contiene prima stringhe poi RegExp, le stringhe sono risolte
|
|
688
|
+
> con O(1) `stat` per pattern e il RegExp usa `readdir` solo se nessuna stringa
|
|
689
|
+
> ha trovato corrispondenza.
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## 9. FASE 7 — LFU cache: eviction O(1)
|
|
694
|
+
|
|
695
|
+
**Concetto:** l'implementazione attuale di `evictLFU` scansiona l'intera Map
|
|
696
|
+
per trovare l'entry con hits minimo. Per cache grandi con frequente pressure
|
|
697
|
+
sul `maxSize`, questo degrada a O(n) per eviction, O(n²) ammortizzato.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
### [x] 9.1 — #6: Struttura LFU classica con bucket di frequenza
|
|
702
|
+
|
|
703
|
+
**Righe:** `577–597`
|
|
704
|
+
**Problema attuale:**
|
|
705
|
+
```js
|
|
706
|
+
function evictLFU(cache, ...) {
|
|
707
|
+
let minHits = Infinity, minKey = null;
|
|
708
|
+
for (const [key, entry] of cache) { // O(n) scan
|
|
709
|
+
if (entry.hits < minHits) { minKey = key; }
|
|
710
|
+
}
|
|
711
|
+
// evict minKey
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
**Struttura dati proposta:** LFU classico O(1) con:
|
|
716
|
+
- `freqMap`: `Map<frequency, Set<key>>` — bucket per frequenza
|
|
717
|
+
- `keyFreq`: `Map<key, frequency>` — frequenza corrente di ogni key
|
|
718
|
+
- `minFreq`: numero intero — frequenza minima attuale
|
|
719
|
+
|
|
720
|
+
```js
|
|
721
|
+
class LFUCache {
|
|
722
|
+
constructor(maxSize) {
|
|
723
|
+
this.maxSize = maxSize; // in bytes
|
|
724
|
+
this.currentSize = 0;
|
|
725
|
+
this.keyMap = new Map(); // key → { buffer, mtime, size, freq }
|
|
726
|
+
this.freqMap = new Map(); // freq → Set<key>
|
|
727
|
+
this.minFreq = 0;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
get(key) {
|
|
731
|
+
if (!this.keyMap.has(key)) return undefined;
|
|
732
|
+
this._incrementFreq(key);
|
|
733
|
+
return this.keyMap.get(key);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
set(key, entry) {
|
|
737
|
+
while (this.currentSize + entry.buffer.length > this.maxSize
|
|
738
|
+
&& this.keyMap.size > 0) {
|
|
739
|
+
this._evictOne();
|
|
740
|
+
}
|
|
741
|
+
if (this.currentSize + entry.buffer.length > this.maxSize) return;
|
|
742
|
+
|
|
743
|
+
this.keyMap.set(key, { ...entry, freq: 1 });
|
|
744
|
+
this._addToFreqBucket(key, 1);
|
|
745
|
+
this.currentSize += entry.buffer.length;
|
|
746
|
+
if (this.minFreq > 1) this.minFreq = 1;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
delete(key) {
|
|
750
|
+
if (!this.keyMap.has(key)) return;
|
|
751
|
+
const { freq, buffer } = this.keyMap.get(key);
|
|
752
|
+
this.currentSize -= buffer.length;
|
|
753
|
+
this.keyMap.delete(key);
|
|
754
|
+
this.freqMap.get(freq)?.delete(key);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
_incrementFreq(key) {
|
|
758
|
+
const entry = this.keyMap.get(key);
|
|
759
|
+
const oldFreq = entry.freq;
|
|
760
|
+
const newFreq = oldFreq + 1;
|
|
761
|
+
entry.freq = newFreq;
|
|
762
|
+
this.freqMap.get(oldFreq).delete(key);
|
|
763
|
+
if (this.freqMap.get(oldFreq).size === 0) {
|
|
764
|
+
this.freqMap.delete(oldFreq);
|
|
765
|
+
if (this.minFreq === oldFreq) this.minFreq = newFreq;
|
|
766
|
+
}
|
|
767
|
+
this._addToFreqBucket(key, newFreq);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
_addToFreqBucket(key, freq) {
|
|
771
|
+
if (!this.freqMap.has(freq)) this.freqMap.set(freq, new Set());
|
|
772
|
+
this.freqMap.get(freq).add(key);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
_evictOne() {
|
|
776
|
+
const bucket = this.freqMap.get(this.minFreq);
|
|
777
|
+
if (!bucket || bucket.size === 0) return;
|
|
778
|
+
const evictKey = bucket.values().next().value; // FIFO a parità di freq
|
|
779
|
+
this.delete(evictKey);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
**Integrazione:** sostituire `_rawFileCache` (Map) e `_compressedFileCache` (Map)
|
|
785
|
+
con istanze di `LFUCache`. Adattare i siti di chiamata per usare
|
|
786
|
+
`cache.get(key)`, `cache.set(key, entry)`, `cache.delete(key)`.
|
|
787
|
+
La funzione globale `evictLFU` diventa obsoleta e può essere rimossa.
|
|
788
|
+
|
|
789
|
+
> **Prerequisito:** aggiornare i test in `server-cache.test.js` che verificano
|
|
790
|
+
> il comportamento LFU, in particolare il test `"all files return correct content
|
|
791
|
+
> even when eviction occurs"` — la semantica rimane identica, solo l'implementazione
|
|
792
|
+
> interna cambia.
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## 10. Stima impatto complessivo atteso
|
|
797
|
+
|
|
798
|
+
### 10.1 File serving (rawFile cache warm)
|
|
799
|
+
|
|
800
|
+
Già ottimale: risposta da buffer in memoria, zero disk I/O.
|
|
801
|
+
Le fasi 1 e 2 riducono l'overhead del routing di ~5–10% in termini di allocazioni.
|
|
802
|
+
|
|
803
|
+
### 10.2 Directory listing su disco locale (ext4/btrfs/xfs)
|
|
804
|
+
|
|
805
|
+
| Scenario | Attuale | Dopo Fase 5 | Miglioramento |
|
|
806
|
+
|----------|---------|-------------|---------------|
|
|
807
|
+
| 10 file, 2 symlink | ~5 ms | ~3 ms | –40% |
|
|
808
|
+
| 100 file, 10 symlink | ~40 ms | ~5 ms | –87% |
|
|
809
|
+
| 1000 file, 50 symlink | ~350 ms | ~15 ms | –96% |
|
|
810
|
+
|
|
811
|
+
### 10.3 Directory listing su NFS/SMB (latenza stat ≈ 15 ms)
|
|
812
|
+
|
|
813
|
+
| Scenario | Attuale | Dopo Fase 5 | Miglioramento |
|
|
814
|
+
|----------|---------|-------------|---------------|
|
|
815
|
+
| 10 file, 2 symlink | ~180 ms | ~25 ms | –86% |
|
|
816
|
+
| 100 file, 10 symlink | ~1800 ms | ~25 ms | –99% |
|
|
817
|
+
| 1000 file, 50 symlink | ~18 s | ~25 ms | –99.9% |
|
|
818
|
+
|
|
819
|
+
### 10.4 High-throughput file serving (>1000 req/s)
|
|
820
|
+
|
|
821
|
+
Le fasi 1, 2, 3, 4 riducono le allocazioni per-richiesta:
|
|
822
|
+
|
|
823
|
+
- Eliminata 1 chiamata `new URL()` incondizionata (fase 2)
|
|
824
|
+
- Eliminati 2 `split`/`join` di array per il prefisso URL (fase 1)
|
|
825
|
+
- Eliminata 1 funzione oggetto creata per richiesta (`escapeHtml`/`formatSize`, fase 3)
|
|
826
|
+
- Lookup MIME O(1) vs O(n) (fase 4)
|
|
827
|
+
|
|
828
|
+
Stima: **+8–15% throughput** su workload ad alto req/s con file statici.
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## 11. Note implementative e ordine consigliato
|
|
833
|
+
|
|
834
|
+
### Ordine consigliato
|
|
835
|
+
|
|
836
|
+
Le fasi sono state progettate per essere indipendenti, ma l'ordine seguente
|
|
837
|
+
minimizza il rischio e massimizza la verificabilità:
|
|
838
|
+
|
|
839
|
+
**Fase 1 → Fase 2 → Fase 3 → Fase 4 → Fase 5 → Fase 6 → Fase 7**
|
|
840
|
+
|
|
841
|
+
- Fasi 1–4 possono essere fatte in un singolo commit ciascuna (sono localizzate).
|
|
842
|
+
- Fase 5 richiede un refactor più ampio di `show_dir` — committare separatamente
|
|
843
|
+
i due sotto-punti (7.1 prima, poi 7.2).
|
|
844
|
+
- Fase 7 richiede una suite di test aggiuntiva per la classe `LFUCache`.
|
|
845
|
+
|
|
846
|
+
### Test da eseguire dopo ogni fase
|
|
847
|
+
|
|
848
|
+
Dopo ogni fase: `npx jest --no-coverage` deve passare tutti i 451 test esistenti
|
|
849
|
+
senza modifiche. Le ottimizzazioni sono trasparenti al comportamento osservabile.
|
|
850
|
+
|
|
851
|
+
**Eccezioni:**
|
|
852
|
+
|
|
853
|
+
- **Fase 4 (#7):** se un test verifica `compressionConfig.mimeTypes` come Array
|
|
854
|
+
(`.length`, spread), aggiornarlo per usare l'API Set (`.size`, `for...of`).
|
|
855
|
+
- **Fase 7 (#6):** i test `server-cache.test.js` sulle eviction LFU rimangono
|
|
856
|
+
validi — la semantica non cambia, solo la velocità.
|
|
857
|
+
|
|
858
|
+
### Compatibilità Node.js
|
|
859
|
+
|
|
860
|
+
Tutte le ottimizzazioni richiedono Node.js ≥ 18 (già dichiarato in `engines`).
|
|
861
|
+
`Promise.all` su array di Promise (fase 5) e `Set` (fase 4) sono disponibili
|
|
862
|
+
da Node.js 10+.
|
|
863
|
+
L'API dirent `isFile()` / `isDirectory()` / `isSymbolicLink()` è disponibile
|
|
864
|
+
da Node.js 10.10+.
|