Aller au contenu

Chaos Frontend

Le Chaos Frontend dégrade le navigateur de l'utilisateur — contrairement aux six autres familles qui dégradent le backend Spring Boot. Les anomalies sont exécutées par chaos-agent.js, un module embarqué dans l'application React qui poll l'état chaos toutes les 5 secondes et remonte des métriques client toutes les 2 secondes.

Architecture

sequenceDiagram
    participant B as Navigateur (React)
    participant CA as chaos-agent.js
    participant API as "POST /api/chaos/frontend/state"
    participant MON as "POST /api/chaos/client-metrics"
    loop Poll état (5s)
      CA->>API: GET /state
      API-->>CA: { cpuBurn, memoryLeak, domFlood, fetchFlood, doubleFetch }
    end
    loop Push métriques (2s)
      CA->>MON: { fps, longTasksPerSec, heapUsedMB, ... }
    end

Service et endpoint

Module frontend : frontend/src/chaos-agent.js Controller backend : FrontendChaosController.java Endpoint admin : POST /api/chaos/frontend/state Endpoint public : GET /api/chaos/frontend/state

Le FrontendChaosController est un simple store en mémoire — aucune logique métier, aucune validation hors clamping [0, 100]. Le formateur écrit l'état via le panneau chaos-admin, le module chaos-agent.js le lit et applique localement.

Les cinq anomalies

Anomalie Slider Mécanisme principal Plafond
cpuBurn 0–100 Web Worker SHA-like + Long Tasks main thread
memoryLeak 0–100 Objets circulaires + listeners orphelins 1.2 Go / 50 000
domFlood 0–100 Injection de nœuds + reflows agressifs 2 000 nœuds/tick
fetchFlood 0–100 Round-robin sur 12 endpoints /api/products 200 req/s
doubleFetch 0–5 ⭐ Monkey-patch window.fetch (multiplicateur) ×6

Le slider doubleFetch est le seul à utiliser une plage 0–5 au lieu de 0–100 — c'est un compteur de duplications, pas un pourcentage.

cpuBurn — Charge CPU navigateur

Slider : 0 – 100 Métriques associées : fps (↓), longTasksPerSec (↑), cpuWorkerActive (true)

L'anomalie utilise deux mécanismes complémentaires pour saturer le CPU côté client : un Web Worker pour la charge CPU réelle visible dans le gestionnaire de tâches, et une boucle bloquante sur le thread principal pour faire chuter le FPS et générer des Long Tasks observables.

A) Web Worker — charge CPU réelle

Le code du worker est embarqué en string et injecté via une Blob URL (évite un fichier .js séparé). La boucle utilise un hashage maison non optimisable par le JIT (XOR + rotation + Math.imul) à raison de intensity × 32 × 100 itérations toutes les 100 ms.

Le worker tourne sur un thread OS dédié — la charge CPU est donc visible dans le Gestionnaire des tâches Windows ou top sous Linux, attribuée au processus du navigateur. C'est volontaire : permettre à l'étudiant de constater que la dégradation est bien causée par son onglet et non par une autre application.

B) Thread principal — Long Tasks et chute FPS

En parallèle du worker, l'agent exécute toutes les 100 ms une boucle bloquante d'une durée de pct × 150 ms (où pct = intensity / 100). À 100 % d'intensité, c'est donc un bloc de 150 ms toutes les 100 ms — soit ~15 Long Tasks par seconde, et un FPS qui chute à 10–15 même sur un écran 144 Hz.

Cette boucle utilise le même calcul que le worker (XOR + rotation + Math.imul) pour empêcher l'élimination par dead-code de la JIT.

Pédagogie associée

Démonstration de l'impact d'un calcul lourd sur le thread principal et introduction au modèle RAIL (Response, Animation, Idle, Load) de Google. L'étudiant apprend à utiliser le PerformanceObserver pour détecter les Long Tasks et à mesurer le FPS via requestAnimationFrame. La défense classique est d'offload les calculs lourds vers un Web Worker — exactement ce que fait le mécanisme A.

memoryLeak — Fuite mémoire navigateur

Slider : 0 – 100 Métriques associées : heapUsedMB (↑), heapLimitMB

L'anomalie cumule deux mécanismes de fuite pour reproduire les deux patterns les plus fréquents en production React.

A) Objets avec référence circulaire — leakBucket

Toutes les secondes, l'agent ajoute pct × 10 000 objets au tableau leakBucket. Chaque objet pèse environ 1 200 octets et contient une référence circulaire vers le bucket parent — empêche le moteur JS de libérer la mémoire même sous pression GC.

leakBucket.push({
  id: Math.random(),
  data: new Array(100).fill('leak_' + Math.random()),
  timestamp: Date.now(),
  ref: leakBucket   // référence circulaire intentionnelle
});

Le bucket est plafonné à 1.2 Go (MAX_BYTES) — limite calculée pour rester sous le quota par onglet de Chrome (1.5–4 Go selon l'OS) et éviter de tuer l'onglet de manière imprévisible.

B) Listeners orphelins sur DOM détaché — leakListeners

En parallèle, l'agent crée pct × 20 éléments <div> par seconde, chacun avec un addEventListener('click') qui capture une closure contenant new Array(500). Les éléments ne sont jamais attachés au DOM — le GC ne peut pas les libérer car le listener et la closure forment un cycle de référence avec l'élément détaché.

C'est exactement le pattern de fuite causé par un useEffect React sans cleanup, ou par un store Redux qui accumule des subscribers sans unsubscribe. Plafond : 50 000 listeners (MAX_LISTENERS).

Pédagogie associée

Diagnostic d'une fuite mémoire JS via Chrome DevTools : prendre deux heap snapshots à 30 secondes d'intervalle, comparer la croissance, identifier les détaché DOM trees et les retainers. Discussion sur les patterns React qui causent ces fuites en pratique.

domFlood — Saturation du moteur de rendu

Slider : 0 – 100 Métriques associées : domNodeCount (oscille), longTasksPerSec (↑)

L'agent injecte un container #chaos-dom-container en bas du body avec opacity: 0.01 (visible mais quasi invisible, pour ne pas perturber la UX réelle de l'application). Toutes les 100 ms, il vide le container et y réinjecte pct × 2000 <div> à styles aléatoires.

Mais le vrai impact ne vient pas du nombre de nœuds : il vient des reflows agressifs qui suivent l'injection. Pour chaque enfant, l'agent fait une séquence de layout thrashing maximum :

void children[i].offsetHeight;                       // reflow lecture
children[i].style.marginLeft = Math.random() + 'px'; // invalide layout
void children[i].offsetWidth;                        // reflow lecture
children[i].style.paddingTop = Math.random() * 2 + 'px'; // invalide layout

Cette alternance lecture / écriture force le moteur à recalculer le layout entre chaque opération au lieu de pouvoir batcher. À 100 % d'intensité avec 2 000 nœuds, c'est 8 000 opérations layout par tick de 100 ms.

Pédagogie associée

Démonstration du layout thrashing et de l'importance d'éviter l'alternance lecture / écriture sur le DOM. Introduction à la virtualisation (react-window, react-virtualized) pour les listes longues, et au pattern useMemo / useCallback pour limiter les re-renders inutiles.

fetchFlood — Inondation HTTP

Slider : 0 – 100 Métriques associées : pendingFetches (req/s réelles)

L'agent envoie en boucle des requêtes GET vers le backend selon la formule reqPerSec = max(1, floor(pct × 200)) — jusqu'à 200 req/s à 100 % d'intensité. Les requêtes alternent en round-robin sur 12 endpoints différents, tous sur /api/products avec pagination et filtres variés :

const FLOOD_ENDPOINTS = [
  `${API}/api/products?size=20`,
  `${API}/api/products?page=1&size=20`,
  `${API}/api/products?category=AVION&size=10`,
  `${API}/api/products?category=VOITURE&size=10`,
  `${API}/api/products/1`,
  // ... 12 endpoints au total
];

Un paramètre _t={timestamp} est ajouté à chaque appel pour contourner le cache HTTP et forcer le serveur à traiter chaque requête. L'effet est doublement visible : pic de débit HTTP côté backend dans http_server_requests_seconds_count, et compteur pendingFetches qui reflète les req/s réellement émises côté client.

Pédagogie associée

Diagnostic d'un bug d'API spam — souvent causé par un useEffect sans dépendances correctes, ou par un polling mal calibré. Discussion sur le debouncing, le throttling et l'utilisation de bibliothèques de cache comme SWR ou React Query pour éliminer les requêtes redondantes.

doubleFetch — Multiplication d'appels API ⭐ NOUVEAU

Slider : 0 – 5 (et non 0 – 100) Mécanisme : monkey-patch de window.fetch

Contrairement aux quatre autres anomalies frontend, doubleFetch ne dégrade pas le navigateur en ajoutant du calcul ou de la mémoire. Il modifie le comportement de fetch pour multiplier silencieusement chaque appel API par un facteur configurable. Le slider est un compteur de duplications, pas un pourcentage :

Valeur slider Multiplicateur effectif Effet par appel API
0 ×1 Comportement normal
1 ×2 1 dupliqué
2 ×3 2 dupliqués
3 ×4 3 dupliqués
4 ×5 4 dupliqués
5 ×6 5 dupliqués

Mécanisme

L'agent remplace window.fetch par une fonction wrapper :

window.fetch = function(input, init) {
  const result = _originalFetch(input, init);
  if (_doubleFetchMultiplier > 0) {
    for (let i = 0; i < _doubleFetchMultiplier; i++) {
      const delay = 10 + (i * 15) + Math.floor(Math.random() * 30);
      setTimeout(() => {
        _originalFetch(input, init).catch(() => {});
      }, delay);
    }
  }
  return result;
};

Les appels dupliqués sont fire-and-forget avec un délai croissant (10 + i × 15 + random(30) ms) — cela simule des event listeners qui se déclenchent en cascade plutôt qu'une rafale instantanée détectable trivialement.

Exclusions critiques

Trois types d'URL sont exclus de la duplication pour éviter une boucle infinie sur le polling chaos lui-même :

  • /chaos/ — endpoints du chaos (état frontend, monitoring)
  • /client-metrics — push des métriques client vers le monitoring
  • /actuator — health checks et scraping Prometheus

Sans ces exclusions, le polling 5 s du chaos-agent serait lui-même multiplié, déclenchant une avalanche de requêtes qui rendrait le diagnostic impossible.

Restauration propre

Quand le slider revient à 0, l'agent restaure le fetch original (window.fetch = _originalFetch) et met _doubleFetchMultiplier = 0. La référence originale est capturée au chargement du module via window.fetch.bind(window) — elle est donc immuable et toujours restaurable.

Pédagogie associée

Reproduction d'un bug de production particulièrement vicieux : double mount d'un composant React (typiquement causé par React.StrictMode en développement), double event listener non nettoyé, ou intercepteur Axios mal configuré qui propage les appels deux fois. Diagnostic via l'onglet Network de Chrome DevTools en filtrant sur les URLs exactes — les appels en double apparaissent groupés à quelques ms d'intervalle.

Métriques client

L'agent expose en temps réel un objet window.__chaosMetrics mis à jour par plusieurs collecteurs en arrière-plan, et POSTe ces métriques vers /api/chaos/client-metrics toutes les 2 secondes pour alimenter le monitoring formateur.

Champ Méthode de collecte Fréquence
fps Compteur requestAnimationFrame rafraîchi chaque seconde 1 s
longTasksPerSec PerformanceObserver({ entryTypes: ['longtask'] }) 1 s
heapUsedMB performance.memory.usedJSHeapSize / 1048576 (Chrome) 500 ms
heapLimitMB performance.memory.jsHeapSizeLimit / 1048576 (Chrome) 500 ms
pendingFetches Compteur de requêtes émises par fetchFlood 1 s
domNodeCount document.querySelectorAll('*').length 2 s
cpuWorkerActive Booléen — true si le Web Worker cpuBurn est actif événementiel
timestamp Date.now() — horodatage de la dernière mesure 500 ms

Les champs heapUsedMB et heapLimitMB ne sont disponibles que sur Chromium (Chrome, Edge, Brave) — l'API performance.memory n'est pas standardisée et n'est pas exposée sur Firefox ou Safari. Sur ces navigateurs, ces champs restent à 0.

L'objet window.__chaosMetrics est inspectable directement depuis la console DevTools — utile pour les démonstrations en formation.

API — endpoints

Lecture de l'état (public)

curl https://perfshop-api.perfshop.io/api/chaos/frontend/state
# {
#   "cpuBurn":     0,
#   "memoryLeak":  0,
#   "domFlood":    0,
#   "fetchFlood":  0,
#   "doubleFetch": 0
# }

Cet endpoint est public — c'est celui que chaos-agent.js interroge toutes les 5 secondes.

Modification de l'état (admin)

curl -X POST https://perfshop-api.perfshop.io/api/chaos/frontend/state \
  -H "X-Admin-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"cpuBurn": 80, "memoryLeak": 50, "domFlood": 0, "fetchFlood": 0, "doubleFetch": 0}'

Le controller clamp chaque valeur dans [0, 100] et n'accepte que les clés connues (cpuBurn, memoryLeak, domFlood, fetchFlood, doubleFetch). Les clés inconnues sont silencieusement ignorées.

Reset

Le reset frontend est inclus dans POST /api/admin/chaos/reset — qui appelle frontendChaosController.resetState() et remet les cinq sliders à 0. Il n'existe pas d'endpoint reset dédié au frontend seul.

Configuration de l'agent

Le module chaos-agent.js lit deux variables d'environnement Vite à la compilation du frontend :

Variable Défaut Usage
VITE_API_URL https://perfshop-api.perfshop.io Backend Spring Boot
VITE_MONITORING_URL http://localhost:3001 Monitoring temps réel

Les intervalles sont définis en constantes en tête de fichier :

const POLL_INTERVAL    = 5000; // poll état chaos toutes les 5s
const METRICS_INTERVAL = 2000; // push métriques client toutes les 2s

L'agent est initialisé au chargement du module — un simple import du fichier dans main.jsx suffit à activer le polling. Il n'y a pas d'API de configuration runtime : tout est piloté via l'état chaos backend.

Pertinence pédagogique

Anomalie Compétence diagnostiquée
cpuBurn Modèle RAIL, throttling JS, offload Web Worker, profil Performance
memoryLeak Heap snapshots Chrome DevTools, retainers, détaché DOM trees
domFlood Layout thrashing, virtualisation de listes, useMemo/useCallback
fetchFlood Debouncing, throttling, cache HTTP, SWR / React Query
doubleFetch Diagnostic React StrictMode, audit des event listeners, intercepteurs