20 - Tester la mémoire
Ce que tu vas apprendre
- Écrire un test qui prouve qu'une fuite mémoire est fixee
- Utiliser process.memoryUsage() pour des assertions dans tes tests
- memlab de Meta : détection automatisee de fuites
- Verifier que la mémoire se stabilise apres N itérations
- Intégrer un budget mémoire dans ta CI
- performance.measureUserAgentSpecificMemory() cote navigateur
Prerequisites
- Savoir écrire des tests (vitest, jest, ou similaire)
- Avoir lu Chrome DevTools et Profiling Node.js
Le bug qui revenait tous les 3 mois
On avait fixe une fuite mémoire dans un service Node.js. Un Map global qui grossissait sans fin. Le fix : un TTL de 5 minutes. Code review, merge, déploiement. La mémoire se stabilise. Tout le monde est content.
Trois mois plus tard, un autre développeur ajoute un second Map pour un cache de permissions. Meme pattern, meme oubli, meme fuite. On avait fixe le symptome mais on n'avait pas mis de garde-fou pour empecher la regression.
C'est la que les tests mémoire entrent en jeu.
Tester qu'une fuite est fixee
Le principe : tu prends un snapshot du heap avant, tu exécutés l'opération suspecte N fois, tu prends un snapshot apres, et tu verifies que la différence est raisonnable.
typescriptimport { describe, it, expect } from "vitest";
describe("Cache memoire", () => {
it("ne devrait pas fuir apres 10 000 operations", () => {
const cache = new BoundedCache<string, string>({ maxSize: 100 });
// Warmup : remplir le cache une premiere fois
for (let i = 0; i < 200; i++) {
cache.set(`key-${i}`, `value-${i}`);
}
// Forcer un GC pour avoir une baseline propre
if (global.gc) global.gc();
const before = process.memoryUsage().heapUsed;
// Executer beaucoup d'operations
for (let i = 0; i < 10_000; i++) {
cache.set(`key-${i}`, `value-${i}`);
}
if (global.gc) global.gc();
const after = process.memoryUsage().heapUsed;
// Le heap ne devrait pas avoir grandi de plus de 1 Mo
const growth = after - before;
expect(growth).toBeLessThan(1024 * 1024);
});
});
Pour que global.gc() fonctionne, tu dois lancer Node.js avec --expose-gc. Dans ton vitest.config.ts :
typescriptexport default defineConfig({
test: {
pool: "forks",
poolOptions: {
forks: {
execArgv: ["--expose-gc"],
},
},
},
});
Verifier la stabilisation
Une fuite, c'est une croissance lineaire. Si la mémoire monte de 1 Mo a chaque lot de 1000 opérations, c'est une fuite. Si elle monte un peu puis se stabilise, c'est normal (le GC n'a pas encore collecte).
typescriptfunction measureGrowth(
fn: () => void,
iterations: number,
batchSize: number
): number[] {
const measurements: number[] = [];
for (let i = 0; i < iterations; i++) {
for (let j = 0; j < batchSize; j++) {
fn();
}
if (global.gc) global.gc();
measurements.push(process.memoryUsage().heapUsed);
}
return measurements;
}
// Si la memoire se stabilise, les 5 derniers points sont proches
function isStable(measurements: number[], toleranceBytes = 512 * 1024): boolean {
const last5 = measurements.slice(-5);
const min = Math.min(...last5);
const max = Math.max(...last5);
return max - min < toleranceBytes;
}
// Usage dans un test
it("la memoire se stabilise", () => {
const measurements = measureGrowth(
() => myService.processItem({ id: Math.random().toString() }),
20, // 20 lots
1000 // 1000 operations par lot
);
expect(isStable(measurements)).toBe(true);
});
memlab : détection automatisee par Meta
memlab est un framework de Meta (Facebook) pour détecter les fuites mémoire dans les applications web. Il automatise le cycle : naviguer vers une page, prendre un snapshot, naviguer ailleurs, prendre un snapshot, comparer.
typescript// memlab.scenario.ts
import { IScenario, Page } from "@aspect-build/memlab";
const scenario: IScenario = {
url: () => "http://localhost:3000",
async action(page: Page) {
// Naviguer vers la page suspecte
await page.click('[data-testid="open-modal"]');
await page.waitForTimeout(2000);
},
async back(page: Page) {
// Revenir a l'etat initial
await page.click('[data-testid="close-modal"]');
await page.waitForTimeout(2000);
},
};
export default scenario;
bashnpx memlab run --scenario memlab.scenario.ts
memlab prend 3 snapshots : avant l'action, apres l'action, apres le retour. Il identifié les objets créés pendant l'action qui n'ont pas ete liberes apres le retour. C'est exactement la définition d'une fuite.
Le rapport te donne les retainer chains : pourquoi chaque objet fuite et quelle référencé le retient. C'est comme un heap snapshot dans DevTools, mais automatise et reproductible.
Sur paltemps.fr, j'ai utilise memlab pour vérifier que la modale de détails d'une station meteo ne fuitait pas. Sans memlab, j'aurais du ouvrir DevTools, prendre les snapshots manuellement, et comparer a la main. Avec memlab, c'est un test qui tourne en CI.
Budget mémoire en CI
L'idee : tu définis une limite de mémoire pour ton application, et la CI echoue si elle est dépassée.
typescript// tests/memory-budget.test.ts
import { describe, it, expect } from "vitest";
import { execSync } from "child_process";
describe("Budget memoire", () => {
it("le serveur au repos ne depasse pas 100 Mo", async () => {
const server = await startServer();
// Attendre que le serveur soit stable
await new Promise((r) => setTimeout(r, 5000));
const usage = await getServerMemory(server.pid);
expect(usage.heapUsed).toBeLessThan(100 * 1024 * 1024);
await server.close();
});
it("apres 1000 requetes le heap ne depasse pas 200 Mo", async () => {
const server = await startServer();
for (let i = 0; i < 1000; i++) {
await fetch("http://localhost:3000/api/data");
}
if (global.gc) global.gc();
const usage = await getServerMemory(server.pid);
expect(usage.heapUsed).toBeLessThan(200 * 1024 * 1024);
await server.close();
});
});
Le budget mémoire agit comme un canari. Si quelqu'un ajoute un cache non borne ou une fuite, le test passe au rouge avant que ca arrive en production.
performance.measureUserAgentSpecificMemory()
Cote navigateur, l'API performance.measureUserAgentSpecificMemory() donne une mesure de la mémoire JavaScript. C'est plus precis que performance.memory (qui est déprécié et Chrome-only).
typescript// Disponible uniquement dans des contextes cross-origin isolated
// Headers requis : Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp
async function measureMemory(): Promise<void> {
if (!performance.measureUserAgentSpecificMemory) {
console.log("API non disponible");
return;
}
const result = await performance.measureUserAgentSpecificMemory();
console.log(`Memoire JS totale : ${Math.round(result.bytes / 1024 / 1024)} Mo`);
for (const breakdown of result.breakdown) {
if (breakdown.bytes > 0) {
console.log(` ${breakdown.types.join(", ")} : ${breakdown.bytes} octets`);
}
}
}
Cette API est asynchrone et ne bloque pas le thread principal. Tu peux l'appeler periodiquement dans ton application pour monitorer la mémoire cote client. Combine avec un service de telemetrie, ca te donne une vue sur la mémoire réelle de tes utilisateurs.
Attention : cette API n'est pas disponible partout. Elle nécessité des headers COOP/COEP spécifiques et n'est supportee que dans les navigateurs Chromium pour l'instant.
Organiser les tests mémoire
Mes conventions :
- Les tests mémoire sont lents. Je les mets dans un dossier
tests/memory/séparé. - Je les exécuté uniquement en CI, pas a chaque
pnpm testen local. - Chaque test a un timeout genereux (30s minimum).
- J'utilise
--expose-gcpour forcer le GC avant les mesures. - Les seuils sont calibres sur des mesures reelles, pas sur des estimations.
json{
"scripts": {
"test": "vitest run",
"test:memory": "vitest run --config vitest.memory.config.ts"
}
}
Résumé
- Un test mémoire prouve qu'une fuite est fixee et empeche la regression.
- Mesure la croissance du heap sur N itérations : si ca monte lineairement, c'est une fuite.
- memlab automatise la détection de fuites dans les apps web (3 snapshots, comparaison).
- Un budget mémoire en CI agit comme un garde-fou permanent.
- performance.measureUserAgentSpecificMemory() donne des mesures precises cote navigateur.
Precedent : Comparaison avec d'autres langages Suivant : Glossaire