Mémoire et performance JS/TS - 20 - Tester la mémoire

Tests de regression mémoire, memlab de Meta, budgets mémoire en CI, et comment prouver qu'une fuite est fixee.

  1. 01 Mémoire et performance JS/TS - 00 - Pourquoi la mémoire compte meme avec un garbage collector
  2. 02 Mémoire et performance JS/TS - 01 - Stack vs Heap
  3. 03 Mémoire et performance JS/TS - 02 - Le cycle de vie de la mémoire
  4. 04 Mémoire et performance JS/TS - 03 - Le garbage collector
  5. 05 Mémoire et performance JS/TS - 04 - V8 en profondeur
  6. 06 Mémoire et performance JS/TS - 05 - Les 6 fuites mémoire classiques
  7. 07 Mémoire et performance JS/TS - 06 - Closures et mémoire
  8. 08 Mémoire et performance JS/TS - 07 - WeakRef, WeakMap et WeakSet
  9. 09 Mémoire et performance JS/TS - 08 - FinalizationRegistry : savoir quand le GC passe
  10. 10 Mémoire et performance JS/TS - 09 - DevTools Memory : investiguer dans Chrome
  11. 11 Mémoire et performance JS/TS - 10 - Profiling mémoire en Node.js
  12. 12 Mémoire et performance JS/TS - 11 - Détecter et corriger les fuites mémoire
  13. 13 Mémoire et performance JS/TS - 12 - ArrayBuffer et TypedArrays
  14. 14 Mémoire et performance JS/TS - 13 - Workers et mémoire partagee
  15. 15 Mémoire et performance JS/TS - 14 - Streams et backpressure
  16. 16 Mémoire et performance JS/TS - 15 - Fuites mémoire en React
  17. 17 Mémoire et performance JS/TS - 16 - Serveurs Node.js et mémoire
  18. 18 Mémoire et performance JS/TS - 17 - Mémoire et Docker
  19. 19 Mémoire et performance JS/TS - 18 - Optimisations mémoire
  20. 20 Mémoire et performance JS/TS - 19 - Comparaison avec d'autres langages
  21. 21 Mémoire et performance JS/TS - 20 - Tester la mémoire
  22. 22 Mémoire et performance JS/TS - 21 - Glossaire

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


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 test en local.
  • Chaque test a un timeout genereux (30s minimum).
  • J'utilise --expose-gc pour 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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.