Spaces:
Running
Running
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Memory probe — how much will this browser give us?</title> | |
| <style> | |
| body { font: 15px/1.5 -apple-system, system-ui, sans-serif; max-width: 40rem; | |
| margin: 2rem auto; padding: 0 1rem; background: #131233; color: #eae7fb; } | |
| h1 { font-size: 1.3rem; } | |
| button { font: inherit; padding: .6rem 1rem; margin: .3rem .3rem .3rem 0; | |
| background: #a48dff; color: #131233; border: 0; border-radius: 6px; } | |
| pre { background: #1c1a47; padding: .8rem; border-radius: 6px; white-space: pre-wrap; | |
| border: 1px solid #332e66; } | |
| .big { font-size: 1.6rem; color: #a48dff; } | |
| small { color: #a7a1cc; } | |
| </style> | |
| <h1>Memory probe</h1> | |
| <p>This page finds out how much memory this browser will actually hand to a web | |
| page before killing the tab. It stores its progress as it goes, so if the tab | |
| dies the number survives. <strong>If the page reloads itself, come back and read | |
| the result.</strong></p> | |
| <div id="verdict"></div> | |
| <button id="inc">Run incremental probe</button> | |
| <button id="one">Run single-block probe</button> | |
| <button id="reset">Clear</button> | |
| <pre id="out">idle</pre> | |
| <script> | |
| const out = document.getElementById("out"); | |
| const verdict = document.getElementById("verdict"); | |
| const say = (m) => { out.textContent += "\n" + m; }; | |
| const K = { | |
| hw: "probe_highwater_mb", running: "probe_running", | |
| mode: "probe_mode", block: "probe_block_mb", | |
| }; | |
| // localStorage is synchronous, so a value written before an out-of-memory kill | |
| // is still there when the tab comes back. | |
| function report() { | |
| const running = localStorage.getItem(K.running); | |
| const hw = localStorage.getItem(K.hw); | |
| const mode = localStorage.getItem(K.mode); | |
| const blk = localStorage.getItem(K.block); | |
| if (running === "1" && hw) { | |
| verdict.innerHTML = | |
| `<p class="big">This browser died at about ${hw} MB.</p>` + | |
| `<p><small>mode: ${mode || "?"}${blk ? ", last single block tried: " + blk + " MB" : ""}. ` + | |
| `That is the ceiling we have to fit under.</small></p>`; | |
| localStorage.setItem(K.running, "0"); | |
| } else if (hw) { | |
| verdict.innerHTML = | |
| `<p class="big">Reached ${hw} MB without dying.</p>` + | |
| `<p><small>mode: ${mode || "?"}. The probe finished or was stopped.</small></p>`; | |
| } | |
| } | |
| report(); | |
| const env = []; | |
| if (navigator.deviceMemory) env.push(`deviceMemory ${navigator.deviceMemory} GB`); | |
| env.push(`cores ${navigator.hardwareConcurrency || "?"}`); | |
| out.textContent = env.join(", "); | |
| if (navigator.storage && navigator.storage.estimate) { | |
| navigator.storage.estimate().then(e => | |
| say(`storage quota ${Math.round(e.quota / 1e6)} MB, used ${Math.round(e.usage / 1e6)} MB`)); | |
| } | |
| const sleep = (ms) => new Promise(r => setTimeout(r, ms)); | |
| // Touch one byte per 4 KB page: allocation alone can be lazy, and a page that | |
| // is never written is not really resident. | |
| function commit(buf) { | |
| const u = new Uint8Array(buf); | |
| for (let i = 0; i < u.length; i += 4096) u[i] = 1; | |
| return u.length; | |
| } | |
| document.getElementById("inc").onclick = async () => { | |
| const STEP = 32; | |
| localStorage.setItem(K.running, "1"); | |
| localStorage.setItem(K.mode, `incremental ${STEP} MB steps`); | |
| localStorage.setItem(K.hw, "0"); | |
| const held = []; | |
| say(`\nallocating in ${STEP} MB steps, touching every page`); | |
| for (let mb = STEP; mb <= 2048; mb += STEP) { | |
| try { | |
| held.push(new ArrayBuffer(STEP * 1024 * 1024)); | |
| commit(held[held.length - 1]); | |
| } catch (e) { | |
| say(`refused at ${mb} MB (${e.name}) — this browser threw instead of dying, which is the good case`); | |
| localStorage.setItem(K.running, "0"); | |
| return report(); | |
| } | |
| localStorage.setItem(K.hw, String(mb)); | |
| out.textContent = env.join(", ") + `\n\nheld: ${mb} MB`; | |
| await sleep(30); | |
| } | |
| say("reached 2048 MB, stopping"); | |
| localStorage.setItem(K.running, "0"); | |
| report(); | |
| }; | |
| document.getElementById("one").onclick = async () => { | |
| localStorage.setItem(K.running, "1"); | |
| localStorage.setItem(K.mode, "single contiguous block"); | |
| localStorage.setItem(K.hw, "0"); | |
| say("\nsingle contiguous allocations (this is what loading one big model file does)"); | |
| for (const mb of [128, 256, 384, 512, 640, 768, 1024]) { | |
| localStorage.setItem(K.block, String(mb)); | |
| try { | |
| const b = new ArrayBuffer(mb * 1024 * 1024); | |
| commit(b); | |
| say(`${mb} MB contiguous: ok`); | |
| localStorage.setItem(K.hw, String(mb)); | |
| } catch (e) { | |
| say(`${mb} MB contiguous: refused (${e.name})`); | |
| break; | |
| } | |
| await sleep(120); | |
| } | |
| localStorage.setItem(K.running, "0"); | |
| report(); | |
| }; | |
| document.getElementById("reset").onclick = () => { | |
| Object.values(K).forEach(k => localStorage.removeItem(k)); | |
| verdict.innerHTML = ""; | |
| say("cleared"); | |
| }; | |
| // Auto-start: the button was easy to miss, and the whole point is that opening | |
| // the page is the entire interaction. | |
| if (localStorage.getItem(K.running) !== "1") { | |
| let n = 3; | |
| const tick = setInterval(() => { | |
| document.getElementById("inc").textContent = | |
| n > 0 ? `Starting in ${n}… (tap to stop)` : "Running"; | |
| if (n-- <= 0) { clearInterval(tick); document.getElementById("inc").click(); } | |
| }, 1000); | |
| document.getElementById("inc").addEventListener("click", () => clearInterval(tick), { once: true }); | |
| } | |
| </script> | |