|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | +<meta charset="UTF-8"> |
| 5 | +<meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | +<title>hx-live recompute warning</title> |
| 7 | +<script src="../../../src/htmx.js"></script> |
| 8 | +<script src="../../../src/ext/hx-live.js"></script> |
| 9 | +<style> |
| 10 | + :root { color-scheme: light dark; font-family: system-ui, sans-serif; } |
| 11 | + body { max-width: 760px; margin: 2rem auto; padding: 0 1rem; line-height: 1.5; } |
| 12 | + button { margin: 0 0.35rem 0.5rem 0; padding: 0.4rem 0.7rem; font: inherit; } |
| 13 | + code, pre { padding: 0.15rem 0.35rem; border-radius: 4px; background: color-mix(in srgb, CanvasText 8%, Canvas); } |
| 14 | + pre { min-height: 4.5rem; padding: 0.75rem; overflow-wrap: anywhere; white-space: pre-wrap; } |
| 15 | + table { width: 100%; border-collapse: collapse; } |
| 16 | + th, td { padding: 0.4rem; border-bottom: 1px solid color-mix(in srgb, CanvasText 20%, Canvas); text-align: left; } |
| 17 | + canvas { width: 100%; height: 60px; border: 1px solid color-mix(in srgb, CanvasText 25%, Canvas); } |
| 18 | + #stress-bindings { display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px; max-height: 130px; overflow: hidden; } |
| 19 | + #stress-bindings output { padding: 2px; background: color-mix(in srgb, CanvasText 6%, Canvas); font: 11px monospace; } |
| 20 | + .status { font-weight: 700; } |
| 21 | + .note { color: color-mix(in srgb, CanvasText 70%, Canvas); } |
| 22 | +</style> |
| 23 | +</head> |
| 24 | +<body hx-ext="hx-live"> |
| 25 | + |
| 26 | +<h1>hx-live recompute warning</h1> |
| 27 | + |
| 28 | +<p>Open DevTools Console, keep this tab visible, then run one scenario. The page also copies the latest hx-live warning below.</p> |
| 29 | + |
| 30 | +<table> |
| 31 | + <thead> |
| 32 | + <tr><th>Trigger</th><th>Warning</th></tr> |
| 33 | + </thead> |
| 34 | + <tbody> |
| 35 | + <tr><td>One recompute pass above 16ms</td><td>Immediate</td></tr> |
| 36 | + <tr><td>Above 100ms total synchronous time in a period</td><td>After two consecutive periods</td></tr> |
| 37 | + </tbody> |
| 38 | +</table> |
| 39 | + |
| 40 | +<p>A measurement period starts with its first recompute pass and closes on the first pass more than one second later. One high period arms the warning. A second consecutive high period emits it.</p> |
| 41 | + |
| 42 | +<p>hx-live warns once per high-load period. <strong>Stop and reset</strong> removes every binding, deactivates hx-live, and clears that warning state.</p> |
| 43 | + |
| 44 | +<h2>Run a scenario</h2> |
| 45 | + |
| 46 | +<p> |
| 47 | + <button id="long-pass">Run one long pass</button> |
| 48 | + <button id="heavy-jank">Start heavy binding jank</button> |
| 49 | + <button id="sustained-work">Start sustained work</button> |
| 50 | + <button id="stop">Stop and reset</button> |
| 51 | +</p> |
| 52 | + |
| 53 | +<p id="status" class="status">Stopped. No live expressions are registered.</p> |
| 54 | + |
| 55 | +<canvas id="fps" width="720" height="60" aria-label="Frames per second"></canvas> |
| 56 | +<p class="note">The meter shows frame rate. Heavy binding jank should push it well below 60 FPS.</p> |
| 57 | + |
| 58 | +<h2>Latest warning</h2> |
| 59 | +<pre id="warning">No warning yet.</pre> |
| 60 | + |
| 61 | +<h2>Live binding output</h2> |
| 62 | +<div id="stress-bindings"></div> |
| 63 | + |
| 64 | +<h2>What each scenario does</h2> |
| 65 | +<ul> |
| 66 | + <li><strong>One long pass:</strong> 200 bindings do about 0.12ms each, once. Expect an immediate warning.</li> |
| 67 | + <li><strong>Heavy binding jank:</strong> 200 bindings do about 0.15ms each, every frame. Expect an immediate warning and visible frame loss.</li> |
| 68 | + <li><strong>Sustained work:</strong> 50 bindings do about 0.05ms each, every frame. Each pass stays below 16ms, but total work exceeds 100ms per period. Expect a warning after about two seconds.</li> |
| 69 | +</ul> |
| 70 | + |
| 71 | +<p class="note">Timings vary by browser and machine. Reload if a preset lands close to a threshold.</p> |
| 72 | + |
| 73 | +<p class="note">From the repository root, run <code>bunx http-server -c-1 .</code>, then open <code>/test/manual/hx-live/recompute-warning.html</code>.</p> |
| 74 | + |
| 75 | +<script> |
| 76 | +document.addEventListener('DOMContentLoaded', () => { |
| 77 | + let tick = 0; |
| 78 | + let workPerBinding = 0; |
| 79 | + let animationFrame = null; |
| 80 | + let warningCount = 0; |
| 81 | + |
| 82 | + const bindings = document.querySelector('#stress-bindings'); |
| 83 | + const status = document.querySelector('#status'); |
| 84 | + const warning = document.querySelector('#warning'); |
| 85 | + const context = document.querySelector('#fps').getContext('2d'); |
| 86 | + const originalWarn = console.warn.bind(console); |
| 87 | + |
| 88 | + console.warn = (...args) => { |
| 89 | + originalWarn(...args); |
| 90 | + if (String(args[0]).startsWith('warning: hx-live overloaded')) { |
| 91 | + warningCount++; |
| 92 | + warning.textContent = `Warning ${warningCount}\n${args[0]}`; |
| 93 | + } |
| 94 | + }; |
| 95 | + |
| 96 | + window.stressBinding = index => { |
| 97 | + let value = tick + index; |
| 98 | + let end = performance.now() + workPerBinding; |
| 99 | + while (performance.now() < end) value = Math.sqrt(value * value + 1); |
| 100 | + return `${index}: ${tick}`; |
| 101 | + }; |
| 102 | + |
| 103 | + function stop() { |
| 104 | + if (animationFrame !== null) cancelAnimationFrame(animationFrame); |
| 105 | + animationFrame = null; |
| 106 | + } |
| 107 | + |
| 108 | + async function reset() { |
| 109 | + stop(); |
| 110 | + bindings.replaceChildren(); |
| 111 | + htmx.live.refresh(); |
| 112 | + await Promise.resolve(); |
| 113 | + status.textContent = 'Stopped. No live expressions are registered.'; |
| 114 | + } |
| 115 | + |
| 116 | + function build(bindingCount, bindingWork) { |
| 117 | + workPerBinding = bindingWork; |
| 118 | + let fragment = document.createDocumentFragment(); |
| 119 | + for (let index = 0; index < bindingCount; index++) { |
| 120 | + let output = document.createElement('output'); |
| 121 | + output.setAttribute(':text', `stressBinding(${index})`); |
| 122 | + fragment.append(output); |
| 123 | + } |
| 124 | + bindings.replaceChildren(fragment); |
| 125 | + htmx.process(bindings); |
| 126 | + } |
| 127 | + |
| 128 | + function startFrames() { |
| 129 | + let run = () => { |
| 130 | + tick++; |
| 131 | + htmx.live.refresh(); |
| 132 | + animationFrame = requestAnimationFrame(run); |
| 133 | + }; |
| 134 | + animationFrame = requestAnimationFrame(run); |
| 135 | + } |
| 136 | + |
| 137 | + document.querySelector('#long-pass').addEventListener('click', async () => { |
| 138 | + await reset(); |
| 139 | + build(200, 0.12); |
| 140 | + status.textContent = 'Running one pass with 200 expensive bindings.'; |
| 141 | + htmx.live.refresh(); |
| 142 | + }); |
| 143 | + |
| 144 | + document.querySelector('#heavy-jank').addEventListener('click', async () => { |
| 145 | + await reset(); |
| 146 | + build(200, 0.15); |
| 147 | + status.textContent = 'Running 200 expensive bindings every frame.'; |
| 148 | + startFrames(); |
| 149 | + }); |
| 150 | + |
| 151 | + document.querySelector('#sustained-work').addEventListener('click', async () => { |
| 152 | + await reset(); |
| 153 | + build(50, 0.05); |
| 154 | + status.textContent = 'Running 50 moderate bindings every frame.'; |
| 155 | + startFrames(); |
| 156 | + }); |
| 157 | + |
| 158 | + document.querySelector('#stop').addEventListener('click', reset); |
| 159 | + |
| 160 | + let frames = 0; |
| 161 | + let measuredAt = performance.now(); |
| 162 | + function drawFps(now) { |
| 163 | + frames++; |
| 164 | + if (now - measuredAt >= 500) { |
| 165 | + let fps = Math.round(frames * 1000 / (now - measuredAt)); |
| 166 | + context.clearRect(0, 0, 720, 60); |
| 167 | + context.font = 'bold 30px system-ui'; |
| 168 | + context.fillStyle = fps < 40 ? '#d33' : '#2a6'; |
| 169 | + context.fillText(`${fps} FPS`, 12, 40); |
| 170 | + frames = 0; |
| 171 | + measuredAt = now; |
| 172 | + } |
| 173 | + requestAnimationFrame(drawFps); |
| 174 | + } |
| 175 | + requestAnimationFrame(drawFps); |
| 176 | +}); |
| 177 | +</script> |
| 178 | + |
| 179 | +</body> |
| 180 | +</html> |
0 commit comments