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 |