Frontend Chaos¶
Frontend Chaos degrades the user's browser — unlike the six other families that degrade the Spring Boot backend. Anomalies are executed by chaos-agent.js, a module embedded in the React application that polls the chaos state every 5 seconds and pushes client metrics every 2 seconds.
Architecture¶
sequenceDiagram
participant B as Browser (React)
participant CA as chaos-agent.js
participant API as "POST /api/chaos/frontend/state"
participant MON as "POST /api/chaos/client-metrics"
loop State poll (5s)
CA->>API: GET /state
API-->>CA: { cpuBurn, memoryLeak, domFlood, fetchFlood, doubleFetch }
end
loop Metrics push (2s)
CA->>MON: { fps, longTasksPerSec, heapUsedMB, ... }
end
Service and endpoint¶
Frontend module: frontend/src/chaos-agent.js
Backend controller: FrontendChaosController.java
Admin endpoint: POST /api/chaos/frontend/state
Public endpoint: GET /api/chaos/frontend/state
FrontendChaosController is a simple in-memory store — no business logic, no validation beyond clamping to [0, 100]. The instructor writes the state through the chaos-admin panel, and the chaos-agent.js module reads and applies it locally.
The five anomalies¶
| Anomaly | Slider | Main mechanism | Cap |
|---|---|---|---|
cpuBurn |
0–100 | SHA-like Web Worker + main-thread Long Tasks | — |
memoryLeak |
0–100 | Circular objects + orphan listeners | 1.2 GB / 50,000 |
domFlood |
0–100 | Node injection + aggressive reflows | 2,000 nodes/tick |
fetchFlood |
0–100 | Round-robin over 12 /api/products endpoints |
200 req/s |
doubleFetch |
0–5 ⭐ | window.fetch monkey-patch (multiplier) |
×6 |
The doubleFetch slider is the only one using a 0–5 range instead of 0–100 — it is a duplication counter, not a percentage.
cpuBurn — Browser CPU load¶
Slider: 0 – 100
Associated metrics: fps (↓), longTasksPerSec (↑), cpuWorkerActive (true)
The anomaly uses two complementary mechanisms to saturate CPU on the client side: a Web Worker for the real CPU load visible in the task manager, and a blocking loop on the main thread to drop FPS and generate observable Long Tasks.
A) Web Worker — real CPU load¶
The worker code is embedded as a string and injected through a Blob URL (avoids a separate .js file). The loop uses a homemade hashing routine (XOR + rotation + Math.imul) that cannot be optimized away by the JIT, running intensity × 32 × 100 iterations every 100 ms.
The worker runs on a dedicated OS thread — the CPU load is therefore visible in Windows Task Manager or top on Linux, attributed to the browser process. This is intentional: it lets the student verify that the degradation really comes from their tab and not from another application.
B) Main thread — Long Tasks and FPS drop¶
In parallel with the worker, the agent runs every 100 ms a blocking loop lasting pct × 150 ms (where pct = intensity / 100). At 100 % intensity, that is a 150 ms block every 100 ms — about ~15 Long Tasks per second, and an FPS that drops to 10–15 even on a 144 Hz display.
This loop uses the same computation as the worker (XOR + rotation + Math.imul) to prevent the JIT from dead-code-eliminating it.
Associated pedagogy¶
Demonstration of the impact of heavy computation on the main thread and introduction to Google's RAIL model (Response, Animation, Idle, Load). The student learns to use the PerformanceObserver to detect Long Tasks and to measure FPS through requestAnimationFrame. The classic defense is to offload heavy computation to a Web Worker — exactly what mechanism A does.
memoryLeak — Browser memory leak¶
Slider: 0 – 100
Associated metrics: heapUsedMB (↑), heapLimitMB
The anomaly combines two leak mechanisms to reproduce the two most frequent patterns seen in React production.
A) Objects with a circular reference — leakBucket¶
Every second, the agent appends pct × 10,000 objects to the leakBucket array. Each object weighs around 1,200 bytes and contains a circular reference to the parent bucket — preventing the JS engine from releasing the memory even under GC pressure.
leakBucket.push({
id: Math.random(),
data: new Array(100).fill('leak_' + Math.random()),
timestamp: Date.now(),
ref: leakBucket // intentional circular reference
});
The bucket is capped at 1.2 GB (MAX_BYTES) — a limit calculated to stay under Chrome's per-tab quota (1.5–4 GB depending on the OS) and to avoid unpredictably killing the tab.
B) Orphan listeners on detached DOM — leakListeners¶
In parallel, the agent creates pct × 20 <div> elements per second, each with an addEventListener('click') that captures a closure containing new Array(500). The elements are never attached to the DOM — the GC cannot release them because the listener and the closure form a reference cycle with the detached element.
This is exactly the leak pattern caused by a React useEffect without cleanup, or by a Redux store that accumulates subscribers without unsubscribe. Cap: 50,000 listeners (MAX_LISTENERS).
Associated pedagogy¶
Diagnosing a JS memory leak through Chrome DevTools: take two heap snapshots 30 seconds apart, compare the growth, identify detached DOM trees and retainers. Discussion of the React patterns that cause these leaks in practice.
domFlood — Render engine saturation¶
Slider: 0 – 100
Associated metrics: domNodeCount (oscillates), longTasksPerSec (↑)
The agent injects a #chaos-dom-container at the bottom of body with opacity: 0.01 (visible but barely, so as not to disturb the real application UX). Every 100 ms, it clears the container and reinjects pct × 2000 <div> with random styles.
But the real impact does not come from the node count: it comes from the aggressive reflows that follow injection. For each child, the agent runs a maximum layout thrashing sequence:
void children[i].offsetHeight; // reflow read
children[i].style.marginLeft = Math.random() + 'px'; // invalidates layout
void children[i].offsetWidth; // reflow read
children[i].style.paddingTop = Math.random() * 2 + 'px'; // invalidates layout
This read/write alternation forces the engine to recompute layout between each operation instead of batching. At 100 % intensity with 2,000 nodes, that is 8,000 layout operations per 100 ms tick.
Associated pedagogy¶
Demonstration of layout thrashing and of the importance of avoiding read/write alternation on the DOM. Introduction to list virtualization (react-window, react-virtualized) for long lists, and to the useMemo / useCallback pattern to limit unnecessary re-renders.
fetchFlood — HTTP flooding¶
Slider: 0 – 100
Associated metric: pendingFetches (actual req/s)
The agent sends GET requests to the backend in a loop according to the formula reqPerSec = max(1, floor(pct × 200)) — up to 200 req/s at 100 % intensity. Requests are round-robined over 12 different endpoints, all on /api/products with varied pagination and filters:
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 in total
];
A _t={timestamp} parameter is added to every call to bypass the HTTP cache and force the server to handle each request. The effect is doubly visible: HTTP throughput spike on the backend side in http_server_requests_seconds_count, and a pendingFetches counter reflecting the req/s actually issued on the client side.
Associated pedagogy¶
Diagnosing an API spam bug — often caused by a useEffect with incorrect dependencies, or by badly calibrated polling. Discussion of debouncing, throttling, and the use of cache libraries such as SWR or React Query to eliminate redundant requests.
doubleFetch — API call multiplication ⭐ NEW¶
Slider: 0 – 5 (not 0 – 100)
Mechanism: window.fetch monkey-patch
Unlike the four other frontend anomalies, doubleFetch does not degrade the browser by adding computation or memory. It modifies the behavior of fetch to silently multiply every API call by a configurable factor. The slider is a duplication counter, not a percentage:
| Slider value | Effective multiplier | Effect per API call |
|---|---|---|
| 0 | ×1 | Normal behavior |
| 1 | ×2 | 1 duplicate |
| 2 | ×3 | 2 duplicates |
| 3 | ×4 | 3 duplicates |
| 4 | ×5 | 4 duplicates |
| 5 | ×6 | 5 duplicates |
Mechanism¶
The agent replaces window.fetch with a wrapper function:
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;
};
Duplicate calls are fire-and-forget with a growing delay (10 + i × 15 + random(30) ms) — this simulates event listeners firing in cascade rather than a trivially detectable instant burst.
Critical exclusions¶
Three URL kinds are excluded from duplication to avoid an infinite loop on the chaos polling itself:
/chaos/— chaos endpoints (frontend state, monitoring)/client-metrics— client metrics push toward monitoring/actuator— health checks and Prometheus scraping
Without these exclusions, the 5-second chaos-agent polling would itself be multiplied, triggering a request avalanche that would make diagnosis impossible.
Clean restoration¶
When the slider returns to 0, the agent restores the original fetch (window.fetch = _originalFetch) and sets _doubleFetchMultiplier = 0. The original reference is captured at module load through window.fetch.bind(window) — it is therefore immutable and always restorable.
Associated pedagogy¶
Reproduction of a particularly nasty production bug: React component double mount (typically caused by React.StrictMode in development), uncleaned double event listener, or a misconfigured Axios interceptor that propagates calls twice. Diagnosis through the Chrome DevTools Network tab by filtering on exact URLs — duplicate calls appear grouped a few ms apart.
Client metrics¶
The agent exposes in real time a window.__chaosMetrics object updated by several background collectors, and POSTs these metrics to /api/chaos/client-metrics every 2 seconds to feed the instructor monitoring.
| Field | Collection method | Frequency |
|---|---|---|
fps |
requestAnimationFrame counter refreshed every second |
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 |
Counter of requests issued by fetchFlood |
1 s |
domNodeCount |
document.querySelectorAll('*').length |
2 s |
cpuWorkerActive |
Boolean — true if the cpuBurn Web Worker is active |
event-driven |
timestamp |
Date.now() — timestamp of the latest measurement |
500 ms |
The heapUsedMB and heapLimitMB fields are only available on Chromium (Chrome, Edge, Brave) — the performance.memory API is not standardized and is not exposed on Firefox or Safari. On those browsers, these fields stay at 0.
The window.__chaosMetrics object is inspectable directly from the DevTools console — useful for training demonstrations.
API — endpoints¶
Read the state (public)¶
curl https://perfshop-api.perfshop.io/api/chaos/frontend/state
# {
# "cpuBurn": 0,
# "memoryLeak": 0,
# "domFlood": 0,
# "fetchFlood": 0,
# "doubleFetch": 0
# }
This endpoint is public — it is the one chaos-agent.js polls every 5 seconds.
Modify the state (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}'
The controller clamps every value to [0, 100] and only accepts known keys (cpuBurn, memoryLeak, domFlood, fetchFlood, doubleFetch). Unknown keys are silently ignored.
Reset¶
The frontend reset is included in POST /api/admin/chaos/reset — which calls frontendChaosController.resetState() and returns the five sliders to 0. There is no dedicated frontend-only reset endpoint.
Agent configuration¶
The chaos-agent.js module reads two Vite environment variables at frontend build time:
| Variable | Default | Usage |
|---|---|---|
VITE_API_URL |
https://perfshop-api.perfshop.io |
Spring Boot backend |
VITE_MONITORING_URL |
http://localhost:3001 |
Real-time monitoring |
The intervals are defined as constants at the top of the file:
const POLL_INTERVAL = 5000; // poll chaos state every 5s
const METRICS_INTERVAL = 2000; // push client metrics every 2s
The agent is initialized when the module is loaded — a simple import of the file in main.jsx is enough to enable polling. There is no runtime configuration API: everything is driven through the backend chaos state.
Pedagogical relevance¶
| Anomaly | Targeted skill |
|---|---|
cpuBurn |
RAIL model, JS throttling, Web Worker offload, Performance profile |
memoryLeak |
Chrome DevTools heap snapshots, retainers, detached DOM trees |
domFlood |
Layout thrashing, list virtualization, useMemo/useCallback |
fetchFlood |
Debouncing, throttling, HTTP cache, SWR / React Query |
doubleFetch |
React StrictMode diagnosis, event listener audit, interceptors |