File size: 3,054 Bytes
2ee9bac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Empirical WASM heap probe. Runs in a Dedicated Worker so if growing the
// WebAssembly.Memory hits the engine's hard limit and aborts, the worker
// dies instead of the tab. The main thread treats worker death as "probe
// failed" and falls back to heuristics.
//
// We measure the same allocator that llama.cpp's WASM build uses for its
// linear memory: a single WebAssembly.Memory object, grown one chunk at a
// time. This matches what wllama does and answers the right question —
// "how big can the WASM heap actually become on this device?" — without
// any JS-side proxy (ArrayBuffer is not the same allocator).
//
// Protocol:
//   main → worker: { stepPages?: number, maxPages?: number }
//                  defaults: 2048 pages (128 MiB) and 65536 pages (4 GiB)
//   worker → main: { committedMB: number, failedAtPagesGrowth?: number }
//
// The wasm32 linear memory is capped at 4 GiB by the engine regardless of
// how much physical RAM the device has, so probing past 65536 pages is
// pointless. On iOS Safari the cap will land much lower (~1 GiB) and the
// grow() throws.

const PAGE_BYTES = 64 * 1024;
const DEFAULT_STEP_PAGES = 2048;     // 128 MiB
const DEFAULT_MAX_PAGES = 65536;     // 4 GiB (wasm32 hard cap)

self.onmessage = async (e) => {
  const stepPages = Number(e.data?.stepPages) || DEFAULT_STEP_PAGES;
  const maxPages = Number(e.data?.maxPages) || DEFAULT_MAX_PAGES;

  let memory;
  try {
    memory = new WebAssembly.Memory({ initial: 1, maximum: maxPages });
  } catch (err) {
    // Engine couldn't even reserve the address space. Treat as 64 KiB
    // (the initial page) and let main thread fall back.
    self.postMessage({ committedMB: 0, error: `Memory ctor failed: ${err.message}` });
    return;
  }

  let committedPages = 1;
  let failedAt = null;

  while (committedPages + stepPages <= maxPages) {
    try {
      memory.grow(stepPages);
      committedPages += stepPages;
      // Touch the last byte of the just-grown region to force a real commit
      // — engines can be lazy about backing pages with physical memory until
      // first write, and we want the probe to reflect actual capacity.
      const view = new Uint8Array(memory.buffer);
      view[committedPages * PAGE_BYTES - 1] = 1;
    } catch (err) {
      failedAt = stepPages;
      break;
    }
    // Yield so the main thread / GC can breathe.
    await new Promise((r) => setTimeout(r, 5));
  }

  // Try one final smaller step in case we have headroom under the last
  // failure but above committedPages. Halve until we either succeed or hit
  // the noise floor.
  if (failedAt !== null) {
    let halfStep = Math.floor(stepPages / 2);
    while (halfStep >= 16 && committedPages + halfStep <= maxPages) {
      try {
        memory.grow(halfStep);
        committedPages += halfStep;
      } catch {
        halfStep = Math.floor(halfStep / 2);
      }
    }
  }

  const committedMB = Math.floor((committedPages * PAGE_BYTES) / (1024 * 1024));
  self.postMessage({ committedMB, failedAtPagesGrowth: failedAt });
};